From b69db29428a8ab4aa6bb4bb5f57ffc1e55690436 Mon Sep 17 00:00:00 2001 From: Leonardo Zide Date: Mon, 12 Oct 2026 19:24:44 -0700 Subject: [PATCH] Replace custom Image buffers with QImage Texture decoding and generated atlases previously used a custom packed-pixel container alongside QImage, duplicating storage and image-processing code. Use QImage throughout texture preparation and upload, remove Image and its pixel-format enum, and store font and string masks in Alpha8 atlases. Preserve power-of-two dimensions with smooth scaling and keep the existing OpenGL formats, shaders, and CPU mipmap filtering. Handle padded scanlines, restore the caller's unpack alignment on every upload return, and centralize binding cleanup so failed uploads do not leave stale texture caches. Validation: full application build and diff checks passed. OpenGL readback checks covered RGB/RGBA/alpha data, padded and custom strides, generated and supplied mipmaps, unpack-state restoration, and failure cleanup. All nine grid mip levels matched the previous generator. Visual rendering was checked by the user. --- common/image.cpp | 201 ----------------------------------- common/image.h | 43 -------- common/lc_assetloader.cpp | 9 +- common/lc_assetloader.h | 1 - common/lc_context.cpp | 79 +++++++++----- common/lc_context.h | 1 + common/lc_library.cpp | 2 +- common/lc_profile.cpp | 1 - common/lc_profile.h | 2 + common/lc_stringcache.cpp | 16 ++- common/lc_texture.cpp | 104 ++++++++++++------ common/lc_texture.h | 12 +-- common/lc_texturemanager.cpp | 38 ++++++- common/lc_texturemanager.h | 3 +- common/lc_viewsphere.cpp | 21 ++-- common/project.cpp | 1 - common/texfont.cpp | 10 +- docs/asset-loading.md | 11 +- leocad.pro | 2 - 19 files changed, 204 insertions(+), 353 deletions(-) delete mode 100644 common/image.cpp delete mode 100644 common/image.h diff --git a/common/image.cpp b/common/image.cpp deleted file mode 100644 index 5266370c..00000000 --- a/common/image.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "lc_global.h" -#include "image.h" -#include "lc_file.h" - -static void CopyFromQImage(const QImage& Src, Image& Dest) -{ - const bool Alpha = Src.hasAlphaChannel(); - Dest.Allocate(Src.width(), Src.height(), Alpha ? lcPixelFormat::R8G8B8A8 : lcPixelFormat::R8G8B8); - - quint8* Bytes = (quint8*)Dest.mData; - - for (int y = 0; y < Dest.mHeight; y++) - { - for (int x = 0; x < Dest.mWidth; x++) - { - QRgb Pixel = Src.pixel(x, y); - - *Bytes++ = qRed(Pixel); - *Bytes++ = qGreen(Pixel); - *Bytes++ = qBlue(Pixel); - - if (Alpha) - *Bytes++ = qAlpha(Pixel); - } - } -} - -Image::Image() -{ - mData = nullptr; - mWidth = 0; - mHeight = 0; - mFormat = lcPixelFormat::Invalid; -} - -Image::Image(Image&& Other) -{ - mData = Other.mData; - mWidth = Other.mWidth; - mHeight = Other.mHeight; - mFormat = Other.mFormat; - - Other.mData = nullptr; - Other.mWidth = 0; - Other.mHeight = 0; - Other.mFormat = lcPixelFormat::Invalid; -} - -Image::~Image() -{ - FreeData(); -} - -int Image::GetBPP() const -{ - switch (mFormat) - { - case lcPixelFormat::Invalid: - return 0; - case lcPixelFormat::A8: - return 1; - case lcPixelFormat::L8A8: - return 2; - case lcPixelFormat::R8G8B8: - return 3; - case lcPixelFormat::R8G8B8A8: - return 4; - } - - return 0; -} - -bool Image::HasAlpha() const -{ - switch (mFormat) - { - case lcPixelFormat::Invalid: - return false; - case lcPixelFormat::A8: - return true; - case lcPixelFormat::L8A8: - return true; - case lcPixelFormat::R8G8B8: - return false; - case lcPixelFormat::R8G8B8A8: - return true; - } - - return 0; -} - -void Image::FreeData() -{ - free(mData); - mData = nullptr; - mWidth = 0; - mHeight = 0; - mFormat = lcPixelFormat::Invalid; -} - -void Image::Allocate(int Width, int Height, lcPixelFormat Format) -{ - FreeData(); - - mWidth = Width; - mHeight = Height; - mFormat = Format; - mData = (unsigned char*)malloc(static_cast(mWidth) * mHeight * GetBPP()); -} - -void Image::ResizePow2() -{ - int i, shifted_x, shifted_y; - - shifted_x = mWidth; - for (i = 0; ((i < 16) && (shifted_x != 0)); i++) - shifted_x = shifted_x >> 1; - shifted_x = (i != 0) ? 1 << (i-1) : 1; - - shifted_y = mHeight; - for (i = 0; ((i < 16) && (shifted_y != 0)); i++) - shifted_y = shifted_y >> 1; - shifted_y = (i != 0) ? 1 << (i-1) : 1; - - if ((shifted_x != mWidth) || (shifted_y != mHeight)) - Resize (shifted_x, shifted_y); -} - -void Image::Resize(int width, int height) -{ - int i, j, k, components, stx, sty; - float accumx, accumy; - unsigned char* bits = nullptr; - - components = GetBPP(); - const size_t BufferSize = static_cast(width) * height * components; - - if (BufferSize) - { - bits = (unsigned char*)malloc(BufferSize); - - if (bits) - { - for (j = 0; j < mHeight; j++) - { - accumy = (float)height*j / (float)mHeight; - sty = (int)floor(accumy); - - for (i = 0; i < mWidth; i++) - { - accumx = (float)width*i / (float)mWidth; - stx = (int)floor(accumx); - - for (k = 0; k < components; k++) - bits[(stx + sty*width)*components + k] = mData[(i + j * mWidth) * components + k]; - } - } - } - } - - free(mData); - mData = bits; - mWidth = width; - mHeight = height; -} - -bool Image::FileLoad(lcFile& File) -{ - QImage Image; - - if (lcMemFile* MemoryFile = dynamic_cast(&File)) - { - if (!MemoryFile->mBuffer || MemoryFile->mPosition >= MemoryFile->mFileSize) - return false; - - const size_t Length = MemoryFile->mFileSize - MemoryFile->mPosition; - - if (Length > static_cast(std::numeric_limits::max()) || !Image.loadFromData(MemoryFile->mBuffer + MemoryFile->mPosition, static_cast(Length))) - return false; - } - else if (lcDiskFile* DiskFile = dynamic_cast(&File)) - { - QFile& Device = DiskFile->GetFile(); - const qint64 Position = Device.pos(); - - if (Position < 0) - return false; - - const bool Loaded = Image.load(&Device, nullptr); - const bool Restored = Device.seek(Position); - - if (!Loaded || !Restored) - return false; - } - else - return false; - - CopyFromQImage(Image, *this); - - return true; -} diff --git a/common/image.h b/common/image.h deleted file mode 100644 index da3e8596..00000000 --- a/common/image.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -// Image Options -#define LC_IMAGE_TRANSPARENT 0x2000 -//#define LC_IMAGE_MASK 0x7000 - -enum class lcPixelFormat -{ - Invalid, - A8, - L8A8, - R8G8B8, - R8G8B8A8 -}; - -class Image -{ -public: - Image(); - Image(Image&& Other); - ~Image(); - - Image(const Image&) = delete; - Image& operator=(const Image&) = delete; - Image& operator=(Image&&) = delete; - - int GetBPP() const; - bool HasAlpha() const; - - // Reads disk or memory files from the current position, preserving the cursor. - bool FileLoad(lcFile& File); - - void Resize(int Width, int Height); - void ResizePow2(); - void Allocate(int Width, int Height, lcPixelFormat Format); - void FreeData(); - - int mWidth; - int mHeight; - lcPixelFormat mFormat; - unsigned char* mData; -}; - diff --git a/common/lc_assetloader.cpp b/common/lc_assetloader.cpp index f6b13d08..8c4d51a2 100644 --- a/common/lc_assetloader.cpp +++ b/common/lc_assetloader.cpp @@ -972,10 +972,13 @@ bool lcAssetLoader::ProcessCompletions() if (Stale) mLibrary->ReleaseTexture(Texture); - else if (Completed.TextureResult.DecodedImage) + else if (!Completed.TextureResult.DecodedImage.isNull()) { - Texture->AdoptDecodedImage(std::move(*Completed.TextureResult.DecodedImage), LC_TEXTURE_MIPMAPS); - UploadTextureRequest(RequestedTexture); + Texture->AdoptDecodedImage(std::move(Completed.TextureResult.DecodedImage), LC_TEXTURE_MIPMAPS); + if (Texture->NeedsUpload()) + UploadTextureRequest(RequestedTexture); + else + FinishTextureRequest(RequestedTexture); } else { diff --git a/common/lc_assetloader.h b/common/lc_assetloader.h index 35152131..bef7652a 100644 --- a/common/lc_assetloader.h +++ b/common/lc_assetloader.h @@ -10,7 +10,6 @@ class lcMesh; class lcSynthInfo; class lcMemFile; class lcLibraryMeshData; -class Image; class lcTexture; enum class lcTextureLoadError; diff --git a/common/lc_context.cpp b/common/lc_context.cpp index 2de7add3..a52d7772 100644 --- a/common/lc_context.cpp +++ b/common/lc_context.cpp @@ -999,19 +999,43 @@ void lcContext::ClearTexture2D() void lcContext::ClearTextureCubeMap() { - if (mTexture2D == 0) + if (mTextureCubeMap == 0) return; glBindTexture(GL_TEXTURE_CUBE_MAP, 0); mTextureCubeMap = 0; } +void lcContext::UploadTextureImage(GLenum Target, int Level, GLenum Format, const QImage& Image) +{ + const size_t RowBytes = static_cast(Image.width()) * (Image.depth() / 8); + const size_t AlignedRowBytes = (RowBytes + 3) & ~size_t(3); + + if (static_cast(Image.bytesPerLine()) == AlignedRowBytes) + { + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTexImage2D(Target, Level, Format, Image.width(), Image.height(), 0, Format, GL_UNSIGNED_BYTE, Image.constBits()); + } + else + { + std::vector PackedPixels(RowBytes * Image.height()); + for (int Row = 0; Row < Image.height(); Row++) + memcpy(PackedPixels.data() + Row * RowBytes, Image.constScanLine(Row), RowBytes); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(Target, Level, Format, Image.width(), Image.height(), 0, Format, GL_UNSIGNED_BYTE, PackedPixels.data()); + } +} + bool lcContext::UploadTexture(lcTexture* Texture) { for (int Error = 0; Error < 16 && glGetError() != GL_NO_ERROR; Error++) { } + GLint UnpackAlignment; + glGetIntegerv(GL_UNPACK_ALIGNMENT, &UnpackAlignment); + if (!Texture->mTexture) glGenTextures(1, &Texture->mTexture); @@ -1026,6 +1050,19 @@ bool lcContext::UploadTexture(lcTexture* Texture) const int FilterIndex = FilterFlags >> LC_TEXTURE_FILTER_SHIFT; const int MipIndex = Flags & LC_TEXTURE_MIPMAPS ? 0 : 1; + const auto FinishUpload = [this, Flags, UnpackAlignment](bool Success) + { + if ((Flags & LC_TEXTURE_CUBEMAP) == 0) + ClearTexture2D(); + else + ClearTextureCubeMap(); + + glPixelStorei(GL_UNPACK_ALIGNMENT, UnpackAlignment); + + const bool NoError = glGetError() == GL_NO_ERROR; + return Success && NoError; + }; + unsigned int Faces, Target; if ((Flags & LC_TEXTURE_CUBEMAP) == 0) @@ -1045,7 +1082,6 @@ bool lcContext::UploadTexture(lcTexture* Texture) glTexParameteri(Target, GL_TEXTURE_WRAP_T, (Flags & LC_TEXTURE_WRAPV) ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameteri(Target, GL_TEXTURE_MIN_FILTER, Filters[MipIndex][FilterIndex]); glTexParameteri(Target, GL_TEXTURE_MAG_FILTER, Filters[1][FilterIndex]); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); if (Flags & LC_TEXTURE_CUBEMAP) { @@ -1058,22 +1094,17 @@ bool lcContext::UploadTexture(lcTexture* Texture) glTexParameterf(Target, GL_TEXTURE_MAX_ANISOTROPY_EXT, lcMin(4.0f, gMaxAnisotropy)); int Format; - switch (Texture->GetImage(0).mFormat) + switch (Texture->GetImage(0).format()) { default: - case lcPixelFormat::Invalid: - Format = 0; - break; - case lcPixelFormat::A8: + return FinishUpload(false); + case QImage::Format_Alpha8: Format = GL_ALPHA; break; - case lcPixelFormat::L8A8: - Format = GL_LUMINANCE_ALPHA; - break; - case lcPixelFormat::R8G8B8: + case QImage::Format_RGB888: Format = GL_RGB; break; - case lcPixelFormat::R8G8B8A8: + case QImage::Format_RGBA8888: Format = GL_RGBA; break; } @@ -1087,15 +1118,15 @@ bool lcContext::UploadTexture(lcTexture* Texture) for (size_t FaceIdx = 0; FaceIdx < Faces; FaceIdx++) { - const void* ImageData = Texture->GetImage(CurrentImage).mData; + const QImage& Image = Texture->GetImage(CurrentImage); - glTexImage2D(Target, 0, Format, Texture->mWidth, Texture->mHeight, 0, Format, GL_UNSIGNED_BYTE, ImageData); + UploadTextureImage(Target, 0, Format, Image); if (LoadMipmaps) { int Width = Texture->mWidth; int Height = Texture->mHeight; - int Components = Texture->GetImage(CurrentImage).GetBPP(); + int Components = Image.depth() / 8; GLubyte* MipBuffer = nullptr; if (!HasMipmaps) @@ -1103,9 +1134,11 @@ bool lcContext::UploadTexture(lcTexture* Texture) MipBuffer = new GLubyte[Width * Height * Components]; if (!MipBuffer) - return false; + return FinishUpload(false); - memcpy(MipBuffer, ImageData, Width * Height * Components); + const size_t RowBytes = static_cast(Width) * Components; + for (int Row = 0; Row < Height; Row++) + memcpy(MipBuffer + Row * RowBytes, Image.constScanLine(Row), RowBytes); } for (int Level = 1; ((Width != 1) || (Height != 1)); Level++) @@ -1142,13 +1175,14 @@ bool lcContext::UploadTexture(lcTexture* Texture) } } + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(Target, Level, Format, Width, Height, 0, Format, GL_UNSIGNED_BYTE, MipBuffer); } else { - const void* Data = Texture->GetImage(++CurrentImage).mData; + const QImage& MipImage = Texture->GetImage(++CurrentImage); - glTexImage2D(Target, Level, Format, Width, Height, 0, Format, GL_UNSIGNED_BYTE, Data); + UploadTextureImage(Target, Level, Format, MipImage); } } @@ -1163,12 +1197,7 @@ bool lcContext::UploadTexture(lcTexture* Texture) Target++; } - if ((Flags & LC_TEXTURE_CUBEMAP) == 0) - ClearTexture2D(); - else - ClearTextureCubeMap(); - - return glGetError() == GL_NO_ERROR; + return FinishUpload(true); } void lcContext::SetColor(float Red, float Green, float Blue, float Alpha) diff --git a/common/lc_context.h b/common/lc_context.h index 72b6d445..5a8c3e80 100644 --- a/common/lc_context.h +++ b/common/lc_context.h @@ -282,6 +282,7 @@ protected: static void DestroyOffscreenContext(); lcResult CreateShaderPrograms(); + void UploadTextureImage(GLenum Target, int Level, GLenum Format, const QImage& Image); void FlushState(); bool TryShareDepthAttachment(GLuint SourceFramebuffer, QOpenGLFramebufferObject& TargetFramebuffer); diff --git a/common/lc_library.cpp b/common/lc_library.cpp index 43c6140d..81e6f39c 100644 --- a/common/lc_library.cpp +++ b/common/lc_library.cpp @@ -701,7 +701,7 @@ lcTextureBuildResult lcPiecesLibrary::BuildTextureData(const lcFileSource& Sourc lcTextureBuildResult Result = mTextureManager->BuildTextureData(*File.value()); - if (!Result.DecodedImage && Source.EmbeddedData) + if (Result.DecodedImage.isNull() && Source.EmbeddedData) Result.ErrorDetails = tr("Could not decode embedded texture '%1'.").arg(Source.EmbeddedData->FileName); return Result; diff --git a/common/lc_profile.cpp b/common/lc_profile.cpp index 86de4e54..f3c87295 100644 --- a/common/lc_profile.cpp +++ b/common/lc_profile.cpp @@ -1,7 +1,6 @@ #include "lc_global.h" #include "lc_profile.h" #include "lc_context.h" -#include "image.h" #include "lc_model.h" #include "project.h" #include "lc_viewsphere.h" diff --git a/common/lc_profile.h b/common/lc_profile.h index b2013f60..4afb6c65 100644 --- a/common/lc_profile.h +++ b/common/lc_profile.h @@ -1,5 +1,7 @@ #pragma once +#define LC_IMAGE_TRANSPARENT 0x2000 + enum class lcShadingMode; enum class lcSelectionRenderMode; enum class lcAxisIconLocation; diff --git a/common/lc_stringcache.cpp b/common/lc_stringcache.cpp index e05c1a70..c2bd8daf 100644 --- a/common/lc_stringcache.cpp +++ b/common/lc_stringcache.cpp @@ -44,14 +44,13 @@ void lcStringCache::CacheStrings(const QStringList& Strings) if (!Update) return; - Image TextureImage; - TextureImage.Allocate(256, 256, lcPixelFormat::L8A8); + QImage TextureImage(256, 256, QImage::Format_Alpha8); QImage Image(128, 128, QImage::Format_ARGB32); QPainter Painter; QFont Font("Helvetica", 20); int DestX = 0, DestY = 0, DestHeight = 0; - memset(TextureImage.mData, 0, TextureImage.mWidth * TextureImage.mHeight * 2); + TextureImage.fill(0); for (auto& Entry : mStrings) { @@ -65,7 +64,7 @@ void lcStringCache::CacheStrings(const QStringList& Strings) Painter.drawText(0, 0, Image.width(), Image.height(), 0, Entry.first, &SourceRect); Painter.end(); - if (DestX + SourceRect.width() + 2 > TextureImage.mWidth) + if (DestX + SourceRect.width() + 2 > TextureImage.width()) { DestX = 0; DestY += DestHeight + 2; @@ -74,25 +73,24 @@ void lcStringCache::CacheStrings(const QStringList& Strings) lcStringCacheEntry& String = Entry.second; - if (SourceRect.width() + 2 > TextureImage.mWidth || DestY + SourceRect.height() + 2 > TextureImage.mHeight || DestY + SourceRect.height() + 2 > TextureImage.mHeight) + if (SourceRect.width() + 2 > TextureImage.width() || DestY + SourceRect.height() + 2 > TextureImage.height() || DestY + SourceRect.height() + 2 > TextureImage.height()) { memset(&String, 0, sizeof(String)); break; } - String.Top = TextureImage.mHeight - DestY - 2; + String.Top = TextureImage.height() - DestY - 2; String.Bottom = String.Top - SourceRect.height() + 1; String.Left = DestX + 1; String.Right = String.Left + SourceRect.width() - 2; for (int y = SourceRect.top(); y < SourceRect.bottom(); y++) { - unsigned char* Dest = TextureImage.mData + ((String.Top - y) * TextureImage.mWidth + String.Left) * 2; + unsigned char* Dest = TextureImage.scanLine(String.Top - y) + String.Left; for (int x = SourceRect.left(); x < SourceRect.right(); x++) { - *Dest = *(Dest + 1) = qRed(Image.pixel(x, y)); - Dest += 2; + *Dest++ = qRed(Image.pixel(x, y)); } } diff --git a/common/lc_texture.cpp b/common/lc_texture.cpp index e02dfe16..07ab4df6 100644 --- a/common/lc_texture.cpp +++ b/common/lc_texture.cpp @@ -1,7 +1,6 @@ #include "lc_global.h" #include "lc_texture.h" #include "lc_context.h" -#include "image.h" #include "lc_glextensions.h" lcTexture* gGridTexture; @@ -20,45 +19,43 @@ void lcTexture::CreateGridTexture() { constexpr int NumLevels = 9; mImages.resize(NumLevels); - quint8* Previous = nullptr; + const QImage* Previous = nullptr; for (int ImageLevel = 0; ImageLevel < NumLevels; ImageLevel++) { - Image& GridImage = mImages[ImageLevel]; + QImage& GridImage = mImages[ImageLevel]; const int GridSize = 256 >> ImageLevel; - GridImage.Allocate(GridSize, GridSize, lcPixelFormat::A8); + GridImage = QImage(GridSize, GridSize, QImage::Format_Alpha8); if (Previous) { - const int PreviousGridSize = 2 * GridSize; - for (int y = 0; y < GridSize - 1; y++) { for (int x = 0; x < GridSize - 1; x++) { - const quint8 a = Previous[x * 2 + y * 2 * PreviousGridSize] > 64 ? 255 : 0; - const quint8 b = Previous[x * 2 + 1 + y * 2 * PreviousGridSize] > 64 ? 255 : 0; - const quint8 c = Previous[x * 2 + (y * 2 + 1) * PreviousGridSize] > 64 ? 255 : 0; - const quint8 d = Previous[x * 2 + 1 + (y * 2 + 1) * PreviousGridSize] > 64 ? 255 : 0; - GridImage.mData[x + y * GridSize] = (a + b + c + d) / 4; + const quint8 a = Previous->constScanLine(y * 2)[x * 2] > 64 ? 255 : 0; + const quint8 b = Previous->constScanLine(y * 2)[x * 2 + 1] > 64 ? 255 : 0; + const quint8 c = Previous->constScanLine(y * 2 + 1)[x * 2] > 64 ? 255 : 0; + const quint8 d = Previous->constScanLine(y * 2 + 1)[x * 2 + 1] > 64 ? 255 : 0; + GridImage.scanLine(y)[x] = (a + b + c + d) / 4; } int x = GridSize - 1; - const quint8 a = Previous[x * 2 + y * 2 * PreviousGridSize]; - const quint8 c = Previous[x * 2 + (y * 2 + 1) * PreviousGridSize]; - GridImage.mData[x + y * GridSize] = (a + c) / 2; + const quint8 a = Previous->constScanLine(y * 2)[x * 2]; + const quint8 c = Previous->constScanLine(y * 2 + 1)[x * 2]; + GridImage.scanLine(y)[x] = (a + c) / 2; } int y = GridSize - 1; for (int x = 0; x < GridSize - 1; x++) { - const quint8 a = Previous[x * 2 + y * 2 * PreviousGridSize]; - const quint8 b = Previous[x * 2 + 1 + y * 2 * PreviousGridSize]; - GridImage.mData[x + y * GridSize] = (a + b) / 2; + const quint8 a = Previous->constScanLine(y * 2)[x * 2]; + const quint8 b = Previous->constScanLine(y * 2)[x * 2 + 1]; + GridImage.scanLine(y)[x] = (a + b) / 2; } int x = GridSize - 1; - GridImage.mData[x + y * GridSize] = Previous[x + y * PreviousGridSize]; + GridImage.scanLine(y)[x] = Previous->constScanLine(y)[x]; } else { @@ -104,13 +101,13 @@ void lcTexture::CreateGridTexture() const quint8 b = TempBuffer[x + 1 + y * GridSize]; const quint8 c = TempBuffer[x + (y + 1) * GridSize]; const quint8 d = TempBuffer[x + 1 + (y + 1) * GridSize]; - GridImage.mData[x + y * GridSize] = (a + b + c + d) / 4; + GridImage.scanLine(y)[x] = (a + b + c + d) / 4; } int x = GridSize - 1; const quint8 a = TempBuffer[x + y * GridSize]; const quint8 c = TempBuffer[x + (y + 1) * GridSize]; - GridImage.mData[x + y * GridSize] = (a + c) / 2; + GridImage.scanLine(y)[x] = (a + c) / 2; } int y = GridSize - 1; @@ -118,15 +115,15 @@ void lcTexture::CreateGridTexture() { const quint8 a = TempBuffer[x + y * GridSize]; const quint8 b = TempBuffer[x + 1 + y * GridSize]; - GridImage.mData[x + y * GridSize] = (a + b) / 2; + GridImage.scanLine(y)[x] = (a + b) / 2; } int x = GridSize - 1; - GridImage.mData[x + y * GridSize] = TempBuffer[x + y * GridSize]; + GridImage.scanLine(y)[x] = TempBuffer[x + y * GridSize]; delete[] TempBuffer; } - Previous = GridImage.mData; + Previous = &GridImage; } mRefCount = 1; @@ -138,19 +135,48 @@ void lcTexture::CreateGridTexture() Upload(Context); } -void lcTexture::SetImage(Image&& Image, int Flags) +QImage lcTexture::PrepareImage(QImage Image) { - mImages.clear(); - mImages.emplace_back(std::move(Image)); - mFlags = Flags; + if (Image.isNull()) + return QImage(); - LoadImages(); + QImage::Format Format = QImage::Format_Alpha8; + if (Image.format() != QImage::Format_Alpha8) + Format = Image.hasAlphaChannel() ? QImage::Format_RGBA8888 : QImage::Format_RGB888; + int Width = 1; + int Height = 1; + + while (Width <= Image.width() / 2 && Width < 32768) + Width *= 2; + while (Height <= Image.height() / 2 && Height < 32768) + Height *= 2; + + if (Width != Image.width() || Height != Image.height()) + Image = Image.scaled(Width, Height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + + return Image.convertToFormat(Format); } -void lcTexture::AdoptDecodedImage(Image&& Image, int Flags) +void lcTexture::SetImage(QImage SourceImage, int Flags) { + AdoptDecodedImage(std::move(SourceImage), Flags); + + if (mState == lcTextureState::Decoded) + LoadImages(); +} + +void lcTexture::AdoptDecodedImage(QImage SourceImage, int Flags) +{ + QImage TextureImage = PrepareImage(std::move(SourceImage)); + + if (TextureImage.isNull()) + { + SetFailed(lcTextureLoadError::DecodeFailed, QString()); + return; + } + mImages.clear(); - mImages.emplace_back(std::move(Image)); + mImages.emplace_back(std::move(TextureImage)); mFlags = Flags; mState = lcTextureState::Decoded; } @@ -167,7 +193,7 @@ void lcTexture::SetFailed(lcTextureLoadError Error, QString Details) mLoadFailureDetails = std::move(Details); } -void lcTexture::SetImage(std::vector&& Images, int Flags) +void lcTexture::SetImage(std::vector&& Images, int Flags) { mImages = std::move(Images); mFlags = Flags; @@ -180,8 +206,8 @@ void lcTexture::Upload(lcContext* Context) if (!NeedsUpload()) return; - mWidth = mImages[0].mWidth; - mHeight = mImages[0].mHeight; + mWidth = mImages[0].width(); + mHeight = mImages[0].height(); const bool Uploaded = Context->UploadTexture(this); @@ -207,8 +233,16 @@ void lcTexture::Upload(lcContext* Context) bool lcTexture::LoadImages() { - for (Image& Image : mImages) - Image.ResizePow2(); + for (QImage& Image : mImages) + { + Image = PrepareImage(std::move(Image)); + + if (Image.isNull()) + { + SetFailed(lcTextureLoadError::DecodeFailed, QString()); + return false; + } + } mState = lcTextureState::Decoded; diff --git a/common/lc_texture.h b/common/lc_texture.h index 09492a8d..d0897d82 100644 --- a/common/lc_texture.h +++ b/common/lc_texture.h @@ -15,7 +15,6 @@ #define LC_TEXTURE_NAME_LEN 256 -#include "image.h" #include "lc_filesource.h" enum class lcTextureState @@ -50,10 +49,11 @@ public: lcTexture& operator=(lcTexture&&) = delete; void CreateGridTexture(); + static QImage PrepareImage(QImage Image); - void SetImage(Image&& Image, int Flags = 0); - void SetImage(std::vector&& Images, int Flags = 0); - void AdoptDecodedImage(Image&& Image, int Flags = 0); + void SetImage(QImage Image, int Flags); + void SetImage(std::vector&& Images, int Flags); + void AdoptDecodedImage(QImage Image, int Flags); void DiscardDecodedImage(); // Records a failure without discarding pixels retained for upload retries. void SetFailed(lcTextureLoadError Error, QString Details); @@ -112,7 +112,7 @@ public: return mFlags; } - const Image& GetImage(int Index) const + const QImage& GetImage(int Index) const { return mImages[Index]; } @@ -137,7 +137,7 @@ protected: bool mTemporary = false; QAtomicInt mRefCount = 0; - std::vector mImages; + std::vector mImages; int mFlags = 0; }; diff --git a/common/lc_texturemanager.cpp b/common/lc_texturemanager.cpp index 57bc60c5..11b7e393 100644 --- a/common/lc_texturemanager.cpp +++ b/common/lc_texturemanager.cpp @@ -64,8 +64,13 @@ lcTextureBuildResult lcTextureManager::BuildTextureData(lcFile& File) { lcTextureBuildResult Result = DecodeTextureData(File); - if (Result.DecodedImage) - Result.DecodedImage->ResizePow2(); + if (!Result.DecodedImage.isNull()) + { + Result.DecodedImage = lcTexture::PrepareImage(std::move(Result.DecodedImage)); + + if (Result.DecodedImage.isNull()) + Result.Error = lcTextureLoadError::DecodeFailed; + } return Result; } @@ -74,11 +79,34 @@ lcTextureBuildResult lcTextureManager::DecodeTextureData(lcFile& File) { lcTextureBuildResult Result; - Result.DecodedImage.reset(new Image); + bool Loaded = false; - if (!Result.DecodedImage->FileLoad(File)) + if (lcMemFile* MemoryFile = dynamic_cast(&File)) { - Result.DecodedImage.reset(); + if (MemoryFile->mBuffer && MemoryFile->mPosition < MemoryFile->mFileSize) + { + const size_t Length = MemoryFile->mFileSize - MemoryFile->mPosition; + + if (Length <= static_cast(std::numeric_limits::max())) + Loaded = Result.DecodedImage.loadFromData(MemoryFile->mBuffer + MemoryFile->mPosition, static_cast(Length)); + } + } + else if (lcDiskFile* DiskFile = dynamic_cast(&File)) + { + QFile& Device = DiskFile->GetFile(); + const qint64 Position = Device.pos(); + + if (Position >= 0) + { + Loaded = Result.DecodedImage.load(&Device, nullptr); + const bool Restored = Device.seek(Position); + Loaded = Loaded && Restored; + } + } + + if (!Loaded || Result.DecodedImage.isNull()) + { + Result.DecodedImage = QImage(); Result.Error = lcTextureLoadError::DecodeFailed; } diff --git a/common/lc_texturemanager.h b/common/lc_texturemanager.h index b4a8223e..5ec6b41e 100644 --- a/common/lc_texturemanager.h +++ b/common/lc_texturemanager.h @@ -14,7 +14,7 @@ struct lcTextureBuildResult lcTextureBuildResult(const lcTextureBuildResult&) = delete; lcTextureBuildResult& operator=(const lcTextureBuildResult&) = delete; - std::unique_ptr DecodedImage; + QImage DecodedImage; lcTextureLoadError Error{}; QString ErrorDetails; }; @@ -42,6 +42,7 @@ public: void ReleaseTexture(lcTexture* Texture); // Worker-safe private CPU build; never publishes or uploads the result. + // Decodes from the current file position without advancing its cursor. lcTextureBuildResult BuildTextureData(lcFile& File); private: diff --git a/common/lc_viewsphere.cpp b/common/lc_viewsphere.cpp index fc603766..42ee373d 100644 --- a/common/lc_viewsphere.cpp +++ b/common/lc_viewsphere.cpp @@ -5,7 +5,6 @@ #include "lc_context.h" #include "lc_stringcache.h" #include "lc_application.h" -#include "image.h" #include "lc_texture.h" lcTexture* lcViewSphere::mTexture; @@ -76,15 +75,14 @@ void lcViewSphere::CreateResources(lcContext* Context) QImage PainterImage(SourceCellSize, SourceCellSize, QImage::Format_ARGB32); QPainter Painter; QFont Font("Helvetica", 36); - Image AtlasImage; - AtlasImage.Allocate(AtlasWidth, AtlasHeight, lcPixelFormat::A8); - memset(AtlasImage.mData, 0, AtlasWidth * AtlasHeight); + QImage AtlasImage(AtlasWidth, AtlasHeight, QImage::Format_Alpha8); + AtlasImage.fill(0); - auto ConvertToSDF = [](Image& TextureImage) + auto ConvertToSDF = [](QImage& TextureImage) { constexpr int Size = 128, Count = Size * Size; std::vector ToInk(Count), ToOutside(Count); - for (int i = 0; i < Count; i++) { ToInk[i] = TextureImage.mData[i] >= 128 ? 0 : 1 << 20; ToOutside[i] = TextureImage.mData[i] >= 128 ? 1 << 20 : 0; } + for (int i = 0; i < Count; i++) { ToInk[i] = TextureImage.constScanLine(i / Size)[i % Size] >= 128 ? 0 : 1 << 20; ToOutside[i] = TextureImage.constScanLine(i / Size)[i % Size] >= 128 ? 1 << 20 : 0; } auto Relax = [](std::vector& Distance, bool Reverse) { for (int y = Reverse ? Size - 1 : 0; Reverse ? y >= 0 : y < Size; y += Reverse ? -1 : 1) @@ -97,13 +95,12 @@ void lcViewSphere::CreateResources(lcContext* Context) } }; Relax(ToInk, false); Relax(ToInk, true); Relax(ToOutside, false); Relax(ToOutside, true); - for (int i = 0; i < Count; i++) TextureImage.mData[i] = static_cast(lcClamp(128 + (ToOutside[i] - ToInk[i]) / 3 * 16, 0, 255)); + for (int i = 0; i < Count; i++) TextureImage.scanLine(i / Size)[i % Size] = static_cast(lcClamp(128 + (ToOutside[i] - ToInk[i]) / 3 * 16, 0, 255)); }; for (int ViewIdx = 0; ViewIdx < 6; ViewIdx++) { - Image TextureImage; - TextureImage.Allocate(SourceCellSize, SourceCellSize, lcPixelFormat::A8); + QImage TextureImage(SourceCellSize, SourceCellSize, QImage::Format_Alpha8); QFont LabelFont = Font; // The shader displays the middle 80% of each cell. Leave six pixels // on either side for the SDF halo, including for translated labels. @@ -122,7 +119,7 @@ void lcViewSphere::CreateResources(lcContext* Context) for (int y = 0; y < SourceCellSize; y++) { - unsigned char* Dest = TextureImage.mData + (SourceCellSize - y - 1) * TextureImage.mWidth; + unsigned char* Dest = TextureImage.scanLine(SourceCellSize - y - 1); for (int x = 0; x < SourceCellSize; x++) *Dest++ = qRed(PainterImage.pixel(x, y)); @@ -131,8 +128,8 @@ void lcViewSphere::CreateResources(lcContext* Context) // Keep the middle band and its SDF margin in a vertical atlas. const int AtlasY = ViewIdx * CellHeight; for (int y = 0; y < CellHeight; y++) - memcpy(AtlasImage.mData + (AtlasY + y) * AtlasWidth, - TextureImage.mData + (CellBottom + y) * SourceCellSize, SourceCellSize); + memcpy(AtlasImage.scanLine(AtlasY + y), + TextureImage.constScanLine(CellBottom + y), SourceCellSize); } mTexture->SetImage(std::move(AtlasImage), LC_TEXTURE_LINEAR); diff --git a/common/project.cpp b/common/project.cpp index 40ea65fb..49f578b9 100644 --- a/common/project.cpp +++ b/common/project.cpp @@ -7,7 +7,6 @@ #include "camera.h" #include "project.h" #include "lc_instructions.h" -#include "image.h" #include "light.h" #include "lc_mainwindow.h" #include "lc_view.h" diff --git a/common/texfont.cpp b/common/texfont.cpp index 9f386777..c3b046af 100644 --- a/common/texfont.cpp +++ b/common/texfont.cpp @@ -3,7 +3,6 @@ #include "texfont.h" #include "lc_context.h" #include "lc_texture.h" -#include "image.h" static const unsigned char GlyphData[] = { @@ -33,9 +32,8 @@ bool TexFont::Initialize(lcContext* Context) constexpr int Scale = 4, Padding = 8, TileWidth = 96, TileHeight = 80; mTextureWidth = mTextureHeight = 1024; - Image Image; - Image.Allocate(mTextureWidth, mTextureHeight, lcPixelFormat::L8A8); - memset(Image.mData, 0, static_cast(mTextureWidth) * mTextureHeight * 2); + QImage Image(mTextureWidth, mTextureHeight, QImage::Format_Alpha8); + Image.fill(0); QFont Font(QStringLiteral("Arial")); Font.setPixelSize(56); @@ -94,8 +92,8 @@ bool TexFont::Initialize(lcContext* Context) { const int SignedDistance = (ToOutside[y * MaskWidth + x] - ToInk[y * MaskWidth + x]) / 3; const unsigned char Value = static_cast(lcClamp(128 + SignedDistance * 8, 0, 255)); - unsigned char* Pixel = Image.mData + ((TileY + y) * mTextureWidth + TileX + x) * 2; - Pixel[0] = Pixel[1] = Value; + unsigned char* Pixel = Image.scanLine(TileY + y) + TileX + x; + *Pixel = Value; } mGlyphs[Glyph].Width = Advance / static_cast(Scale); diff --git a/docs/asset-loading.md b/docs/asset-loading.md index b9e02802..9d037421 100644 --- a/docs/asset-loading.md +++ b/docs/asset-loading.md @@ -122,7 +122,7 @@ Library unload drains loader work, destroys meshes that release texture referenc clears the texture manager, then closes archives. Workers must be drained before the manager is cleared or archive indexes are replaced. -`Image::FileLoad(lcFile&)` decodes disk files through their existing `QFile` and +`lcTextureManager::DecodeTextureData(lcFile&)` decodes disk files through their existing `QFile` and memory files through `QImage::loadFromData()`, without an additional device adapter or disk-to-memory copy. Disk cursor restoration runs on success and failure; memory decoding leaves the cursor unchanged and validates the remaining buffer @@ -131,6 +131,15 @@ private to each request, not shared between workers. Texture readiness waits use the same loader worker path; unused direct texture-loading overloads and their synchronous library/manager forwarding chain have been removed. +Decoded worker results hold a `QImage`, normalized to RGB888 or RGBA8888 and +scaled with Qt's smooth transformation to the existing power-of-two dimensions. +Textures retain the decoded `QImage` through upload without a second pixel buffer. +Uploads honor Qt's scanline alignment, packing unusual row strides only when +needed. CPU mipmap generation copies pixel rows without padding into its existing +working buffer; its filtering, OpenGL formats, and shaders remain unchanged. +Each transfer sets its required unpack alignment, and texture upload restores +the caller's unpack alignment on every exit. Pack alignment is left unchanged. + `lcTexture::SetFailed()` assigns readiness and failure diagnostics together, clearing stale details when none are supplied. Decoding and upload failures use the same setter. It retains decoded pixels for bounded upload diff --git a/leocad.pro b/leocad.pro index d752cf68..48dfee9f 100644 --- a/leocad.pro +++ b/leocad.pro @@ -174,7 +174,6 @@ ios { SOURCES += \ common/camera.cpp \ common/group.cpp \ - common/image.cpp \ common/lc_modelhistory.cpp \ common/light.cpp \ common/minifig.cpp \ @@ -255,7 +254,6 @@ SOURCES += \ HEADERS += \ common/camera.h \ common/group.h \ - common/image.h \ common/lc_modelhistory.h \ common/lc_result.h \ common/light.h \