From 5ba85290ab5a423b133dcb067e2d40c4671a285e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Thu, 9 Apr 2026 16:35:43 -0500 Subject: [PATCH] refactor: Deduplicated BMP header writing in Xtc (#1439) ## Summary **What is the goal of this PR?** Replaced manual 1-bit BMP header logic in `Xtc::generateCoverBmp()` and `Xtc::generateThumbBmp()` with calls to the existing `createBmpHeader()` utility. Added a `BmpRowOrder` enum and param to support the top-down row order of XTC cover images. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ --- lib/GfxRenderer/BitmapHelpers.cpp | 12 ++-- lib/GfxRenderer/BitmapHelpers.h | 4 +- lib/Xtc/Xtc.cpp | 98 ++++--------------------------- src/util/ScreenshotUtil.cpp | 2 +- 4 files changed, 21 insertions(+), 95 deletions(-) diff --git a/lib/GfxRenderer/BitmapHelpers.cpp b/lib/GfxRenderer/BitmapHelpers.cpp index e9dbb64d5..dca059ec9 100644 --- a/lib/GfxRenderer/BitmapHelpers.cpp +++ b/lib/GfxRenderer/BitmapHelpers.cpp @@ -108,7 +108,7 @@ uint8_t quantize1bit(int gray, int x, int y) { return (gray >= adjustedThreshold) ? 1 : 0; } -void createBmpHeader(BmpHeader* bmpHeader, int width, int height) { +void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder) { if (!bmpHeader) return; // Zero out the memory to ensure no garbage data if called on uninitialized stack memory @@ -126,15 +126,15 @@ void createBmpHeader(BmpHeader* bmpHeader, int width, int height) { bmpHeader->infoHeader.biSize = sizeof(bmpHeader->infoHeader); bmpHeader->infoHeader.biWidth = width; - bmpHeader->infoHeader.biHeight = height; + bmpHeader->infoHeader.biHeight = (rowOrder == BmpRowOrder::TopDown) ? -height : height; bmpHeader->infoHeader.biPlanes = 1; bmpHeader->infoHeader.biBitCount = 1; bmpHeader->infoHeader.biCompression = 0; bmpHeader->infoHeader.biSizeImage = imageSize; - bmpHeader->infoHeader.biXPelsPerMeter = 0; - bmpHeader->infoHeader.biYPelsPerMeter = 0; - bmpHeader->infoHeader.biClrUsed = 0; - bmpHeader->infoHeader.biClrImportant = 0; + bmpHeader->infoHeader.biXPelsPerMeter = 2835; // 72 DPI + bmpHeader->infoHeader.biYPelsPerMeter = 2835; // 72 DPI + bmpHeader->infoHeader.biClrUsed = 2; + bmpHeader->infoHeader.biClrImportant = 2; // Color 0 (black) bmpHeader->colors[0].rgbBlue = 0; diff --git a/lib/GfxRenderer/BitmapHelpers.h b/lib/GfxRenderer/BitmapHelpers.h index 8f49124c8..d9d2d8554 100644 --- a/lib/GfxRenderer/BitmapHelpers.h +++ b/lib/GfxRenderer/BitmapHelpers.h @@ -11,8 +11,10 @@ uint8_t quantizeSimple(int gray); uint8_t quantize1bit(int gray, int x, int y); int adjustPixel(int gray); +enum class BmpRowOrder { BottomUp, TopDown }; + // Populates a 1-bit BMP header in the provided memory. -void createBmpHeader(BmpHeader* bmpHeader, int width, int height); +void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder); // 1-bit Atkinson dithering - better quality than noise dithering for thumbnails // Error distribution pattern (same as 2-bit but quantizes to 2 levels): diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 53d32cac3..71e5f28c2 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -7,6 +7,7 @@ #include "Xtc.h" +#include #include #include @@ -172,52 +173,12 @@ bool Xtc::generateCoverBmp() const { return false; } - // Write BMP header - // BMP file header (14 bytes) - const uint32_t rowSize = ((pageInfo.width + 31) / 32) * 4; // Row size aligned to 4 bytes - const uint32_t imageSize = rowSize * pageInfo.height; - const uint32_t fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data + // Write 1-bit BMP header (top-down row order) + BmpHeader bmpHeader; + createBmpHeader(&bmpHeader, pageInfo.width, pageInfo.height, BmpRowOrder::TopDown); + coverBmp.write(reinterpret_cast(&bmpHeader), sizeof(bmpHeader)); - // File header - coverBmp.write('B'); - coverBmp.write('M'); - coverBmp.write(reinterpret_cast(&fileSize), 4); - uint32_t reserved = 0; - coverBmp.write(reinterpret_cast(&reserved), 4); - uint32_t dataOffset = 14 + 40 + 8; // 1-bit palette has 2 colors (8 bytes) - coverBmp.write(reinterpret_cast(&dataOffset), 4); - - // DIB header (BITMAPINFOHEADER - 40 bytes) - uint32_t dibHeaderSize = 40; - coverBmp.write(reinterpret_cast(&dibHeaderSize), 4); - int32_t width = pageInfo.width; - coverBmp.write(reinterpret_cast(&width), 4); - int32_t height = -static_cast(pageInfo.height); // Negative for top-down - coverBmp.write(reinterpret_cast(&height), 4); - uint16_t planes = 1; - coverBmp.write(reinterpret_cast(&planes), 2); - uint16_t bitsPerPixel = 1; // 1-bit monochrome - coverBmp.write(reinterpret_cast(&bitsPerPixel), 2); - uint32_t compression = 0; // BI_RGB (no compression) - coverBmp.write(reinterpret_cast(&compression), 4); - coverBmp.write(reinterpret_cast(&imageSize), 4); - int32_t ppmX = 2835; // 72 DPI - coverBmp.write(reinterpret_cast(&ppmX), 4); - int32_t ppmY = 2835; - coverBmp.write(reinterpret_cast(&ppmY), 4); - uint32_t colorsUsed = 2; - coverBmp.write(reinterpret_cast(&colorsUsed), 4); - uint32_t colorsImportant = 2; - coverBmp.write(reinterpret_cast(&colorsImportant), 4); - - // Color palette (2 colors for 1-bit) - // XTC 1-bit polarity: 0 = black, 1 = white (standard BMP palette order) - // Color 0: Black (text/foreground in XTC) - uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; - coverBmp.write(black, 4); - // Color 1: White (background in XTC) - uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; - coverBmp.write(white, 4); + const uint32_t rowSize = ((pageInfo.width + 31) / 32) * 4; // Write bitmap data // BMP requires 4-byte row alignment @@ -400,49 +361,12 @@ bool Xtc::generateThumbBmp(int height) const { return false; } - // Write 1-bit BMP header for fast home screen rendering - const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes - const uint32_t imageSize = rowSize * thumbHeight; - const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette + // Write 1-bit BMP header (top-down row order) + BmpHeader bmpHeader; + createBmpHeader(&bmpHeader, thumbWidth, thumbHeight, BmpRowOrder::TopDown); + thumbBmp.write(reinterpret_cast(&bmpHeader), sizeof(bmpHeader)); - // File header - thumbBmp.write('B'); - thumbBmp.write('M'); - thumbBmp.write(reinterpret_cast(&fileSize), 4); - uint32_t reserved = 0; - thumbBmp.write(reinterpret_cast(&reserved), 4); - uint32_t dataOffset = 14 + 40 + 8; // 1-bit palette has 2 colors (8 bytes) - thumbBmp.write(reinterpret_cast(&dataOffset), 4); - - // DIB header - uint32_t dibHeaderSize = 40; - thumbBmp.write(reinterpret_cast(&dibHeaderSize), 4); - int32_t widthVal = thumbWidth; - thumbBmp.write(reinterpret_cast(&widthVal), 4); - int32_t heightVal = -static_cast(thumbHeight); // Negative for top-down - thumbBmp.write(reinterpret_cast(&heightVal), 4); - uint16_t planes = 1; - thumbBmp.write(reinterpret_cast(&planes), 2); - uint16_t bitsPerPixel = 1; // 1-bit for black and white - thumbBmp.write(reinterpret_cast(&bitsPerPixel), 2); - uint32_t compression = 0; - thumbBmp.write(reinterpret_cast(&compression), 4); - thumbBmp.write(reinterpret_cast(&imageSize), 4); - int32_t ppmX = 2835; - thumbBmp.write(reinterpret_cast(&ppmX), 4); - int32_t ppmY = 2835; - thumbBmp.write(reinterpret_cast(&ppmY), 4); - uint32_t colorsUsed = 2; - thumbBmp.write(reinterpret_cast(&colorsUsed), 4); - uint32_t colorsImportant = 2; - thumbBmp.write(reinterpret_cast(&colorsImportant), 4); - - // Color palette (2 colors for 1-bit: black and white) - uint8_t palette[8] = { - 0x00, 0x00, 0x00, 0x00, // Color 0: Black - 0xFF, 0xFF, 0xFF, 0x00 // Color 1: White - }; - thumbBmp.write(palette, 8); + const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // Allocate row buffer for 1-bit output uint8_t* rowBuffer = static_cast(malloc(rowSize)); diff --git a/src/util/ScreenshotUtil.cpp b/src/util/ScreenshotUtil.cpp index 0f14f5060..1a564a61b 100644 --- a/src/util/ScreenshotUtil.cpp +++ b/src/util/ScreenshotUtil.cpp @@ -62,7 +62,7 @@ bool ScreenshotUtil::saveFramebufferAsBmp(const char* filename, const uint8_t* f BmpHeader header; - createBmpHeader(&header, phyWidth, phyHeight); + createBmpHeader(&header, phyWidth, phyHeight, BmpRowOrder::BottomUp); bool write_error = false; if (file.write(reinterpret_cast(&header), sizeof(header)) != sizeof(header)) {