From bc6e445f0cdf07664584efc150d4b74a46a0d8a8 Mon Sep 17 00:00:00 2001 From: itsRevela Date: Fri, 17 Apr 2026 07:06:02 -0500 Subject: [PATCH] fix: capture save thumbnails directly from D3D11 backbuffer The new 4JLibs CaptureThumbnail produces all-black 64x64 PNGs because its internal m_backBufferTexture copy fails silently. Bypass it entirely and capture from the swap chain backbuffer using the same proven approach as our F2 screenshot, with center-crop and downsample to 64x64 PNG. --- Minecraft.Client/Windows64/Windows64_App.cpp | 118 ++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/Windows64/Windows64_App.cpp b/Minecraft.Client/Windows64/Windows64_App.cpp index d95aa7f6..5b8882db 100644 --- a/Minecraft.Client/Windows64/Windows64_App.cpp +++ b/Minecraft.Client/Windows64/Windows64_App.cpp @@ -9,6 +9,11 @@ #include "../../Minecraft.World/LevelSettings.h" #include "../../Minecraft.World/BiomeSource.h" #include "../../Minecraft.World/LevelType.h" +#include "stb_image_write.h" + +extern ID3D11Device* g_pd3dDevice; +extern ID3D11DeviceContext* g_pImmediateContext; +extern IDXGISwapChain* g_pSwapChain; CConsoleMinecraftApp app; @@ -33,9 +38,120 @@ void CConsoleMinecraftApp::FatalLoadError() { } +static const int THUMBNAIL_SIZE = 64; + void CConsoleMinecraftApp::CaptureSaveThumbnail() { - RenderManager.CaptureThumbnail(&m_ThumbnailBuffer); + if (!g_pSwapChain || !g_pd3dDevice || !g_pImmediateContext) + return; + + // Release any previous capture + if (m_ThumbnailBuffer.Allocated()) + m_ThumbnailBuffer.Release(); + + // Get the backbuffer + ID3D11Texture2D* pBackBuffer = nullptr; + HRESULT hr = g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&pBackBuffer); + if (FAILED(hr)) + return; + + D3D11_TEXTURE2D_DESC backDesc = {}; + pBackBuffer->GetDesc(&backDesc); + + // Create a staging texture at backbuffer size to read pixels + D3D11_TEXTURE2D_DESC stagingDesc = backDesc; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.BindFlags = 0; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + stagingDesc.MiscFlags = 0; + + ID3D11Texture2D* pStaging = nullptr; + hr = g_pd3dDevice->CreateTexture2D(&stagingDesc, nullptr, &pStaging); + if (FAILED(hr)) + { + pBackBuffer->Release(); + return; + } + + g_pImmediateContext->CopyResource(pStaging, pBackBuffer); + pBackBuffer->Release(); + + D3D11_MAPPED_SUBRESOURCE mapped = {}; + hr = g_pImmediateContext->Map(pStaging, 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED(hr)) + { + pStaging->Release(); + return; + } + + // Downsample to THUMBNAIL_SIZE x THUMBNAIL_SIZE with simple box filter + unsigned char* thumb = new unsigned char[THUMBNAIL_SIZE * THUMBNAIL_SIZE * 4]; + + // Crop to square (center crop), then scale down + UINT srcSize = (backDesc.Width < backDesc.Height) ? backDesc.Width : backDesc.Height; + UINT offsetX = (backDesc.Width - srcSize) / 2; + UINT offsetY = (backDesc.Height - srcSize) / 2; + + for (int ty = 0; ty < THUMBNAIL_SIZE; ty++) + { + for (int tx = 0; tx < THUMBNAIL_SIZE; tx++) + { + // Map thumbnail pixel to source region + UINT sx = offsetX + (tx * srcSize) / THUMBNAIL_SIZE; + UINT sy = offsetY + (ty * srcSize) / THUMBNAIL_SIZE; + + const unsigned char* src = (const unsigned char*)mapped.pData + sy * mapped.RowPitch + sx * 4; + unsigned char* dst = thumb + (ty * THUMBNAIL_SIZE + tx) * 4; + + dst[0] = src[0]; // R (or B depending on format, but BGRA->RGBA swap below) + dst[1] = src[1]; // G + dst[2] = src[2]; // B + dst[3] = 0xFF; // A + } + } + + g_pImmediateContext->Unmap(pStaging, 0); + pStaging->Release(); + + // If backbuffer is BGRA, swap to RGBA for PNG + if (backDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM || backDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB) + { + for (int i = 0; i < THUMBNAIL_SIZE * THUMBNAIL_SIZE; i++) + { + unsigned char tmp = thumb[i * 4]; + thumb[i * 4] = thumb[i * 4 + 2]; + thumb[i * 4 + 2] = tmp; + } + } + + // Encode to PNG in memory using stbi_write_png_to_func + struct PngBuffer { unsigned char* data; int size; int capacity; } pngBuf = {}; + pngBuf.capacity = THUMBNAIL_SIZE * THUMBNAIL_SIZE * 4 + 256; + pngBuf.data = (unsigned char*)malloc(pngBuf.capacity); + pngBuf.size = 0; + + stbi_write_png_to_func([](void* ctx, void* data, int size) { + PngBuffer* buf = (PngBuffer*)ctx; + if (buf->size + size > buf->capacity) + { + buf->capacity = (buf->size + size) * 2; + buf->data = (unsigned char*)realloc(buf->data, buf->capacity); + } + memcpy(buf->data + buf->size, data, size); + buf->size += size; + }, &pngBuf, THUMBNAIL_SIZE, THUMBNAIL_SIZE, 4, thumb, THUMBNAIL_SIZE * 4); + delete[] thumb; + + if (pngBuf.size > 0) + { + m_ThumbnailBuffer.m_type = ImageFileBuffer::e_typePNG; + m_ThumbnailBuffer.m_pBuffer = pngBuf.data; + m_ThumbnailBuffer.m_bufferSize = pngBuf.size; + } + else + { + free(pngBuf.data); + } } void CConsoleMinecraftApp::GetSaveThumbnail(PBYTE *pbData,DWORD *pdwSize) {