feat(debug): theme x tab screenshot sweep + Debug options dialog

Add a debug tool that cycles every skin across every build-enabled tab and saves a
PNG of each to a timestamped folder under the config dir (screenshots/sweep_<ts>/)
— for gathering visual context on theme-specific UI work.

- App state machine (app_network.cpp): startScreenshotSweep() builds the
  skin+enabled-page lists, saves the current skin/page, and steps through them;
  updateScreenshotSweep() (top of App::render) pins the page + settles ~4 frames
  after each skin/page change (skin switches reload TOML + reset the acrylic
  capture, so they need to settle); wantsScreenshotThisFrame()/screenshotSweepPath()
  /onScreenshotCaptured() coordinate with main.cpp. Restores the original skin/page
  when done; nothing persisted.
- Framebuffer capture (main.cpp): read the finished frame and encode PNG via the
  bundled miniz (tdefl_write_image_to_png_file_in_memory_ex). GL reads FBO 0 (RGBA,
  bottom-up -> flip); DX11 copies the backbuffer to a staging texture + maps it
  (BGRA, top-down -> channel-swap). Alpha forced opaque.
- Settings UI: move the Verbose logging checkbox off the wallet row into a new
  "Debug options" button that opens a dialog housing the verbose toggle + a "Run
  screenshot sweep" button. New i18n keys.

Both platforms build; no-crash smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 17:41:49 -05:00
parent a523611779
commit dddf1b6ec7
6 changed files with 236 additions and 9 deletions

View File

@@ -71,11 +71,70 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid,
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <chrono>
#include <miniz.h>
#include "util/logger.h"
// Read the just-rendered framebuffer and write it to a PNG (used by the debug screenshot sweep).
// GL: read FBO 0 (RGBA, bottom-up -> miniz flip). DX11: copy the backbuffer to a staging texture,
// map it (BGRA, top-down -> channel-swap, no flip). Alpha forced opaque (the window is transparent).
namespace {
inline void writePng(const std::string& path, const std::vector<uint8_t>& rgba, int w, int h, bool flip) {
size_t len = 0;
void* png = tdefl_write_image_to_png_file_in_memory_ex(rgba.data(), w, h, 4, &len, 6, flip ? MZ_TRUE : MZ_FALSE);
if (!png) return;
std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (out) out.write(reinterpret_cast<const char*>(png), static_cast<std::streamsize>(len));
mz_free(png);
}
#ifdef DRAGONX_USE_DX11
inline void captureFrameToPng(dragonx::platform::DX11Context& dx, const std::string& path) {
IDXGISwapChain1* sc = dx.swapChain(); ID3D11Device* dev = dx.device(); ID3D11DeviceContext* ctx = dx.deviceContext();
if (!sc || !dev || !ctx) return;
ID3D11Texture2D* back = nullptr;
if (FAILED(sc->GetBuffer(0, IID_PPV_ARGS(&back))) || !back) return;
D3D11_TEXTURE2D_DESC bd; back->GetDesc(&bd);
D3D11_TEXTURE2D_DESC sd = bd; sd.Usage = D3D11_USAGE_STAGING; sd.BindFlags = 0;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.MiscFlags = 0;
ID3D11Texture2D* staging = nullptr;
if (SUCCEEDED(dev->CreateTexture2D(&sd, nullptr, &staging)) && staging) {
ctx->CopyResource(staging, back);
D3D11_MAPPED_SUBRESOURCE m;
if (SUCCEEDED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
int w = (int)bd.Width, h = (int)bd.Height;
std::vector<uint8_t> px((size_t)w * h * 4);
for (int y = 0; y < h; ++y) {
const uint8_t* s = (const uint8_t*)m.pData + (size_t)y * m.RowPitch;
uint8_t* d = px.data() + (size_t)y * w * 4;
for (int x = 0; x < w; ++x) { d[x*4+0]=s[x*4+2]; d[x*4+1]=s[x*4+1]; d[x*4+2]=s[x*4+0]; d[x*4+3]=255; }
}
ctx->Unmap(staging, 0);
writePng(path, px, w, h, /*flip*/false); // DX11 textures are top-down
}
staging->Release();
}
back->Release();
}
#else
inline void captureFrameToPng(const std::string& path, int w, int h) {
if (w <= 0 || h <= 0) return;
std::vector<uint8_t> px((size_t)w * h * 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
for (size_t i = 3; i < px.size(); i += 4) px[i] = 255; // opaque
writePng(path, px, w, h, /*flip*/true); // GL default framebuffer is bottom-up
}
#endif
} // namespace
#ifdef _WIN32
// Windows crash handler — writes a minidump and log entry before exit
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
@@ -1632,6 +1691,12 @@ int main(int argc, char* argv[])
}
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before present.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(dx, app.screenshotSweepPath());
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
@@ -1655,6 +1720,12 @@ int main(int argc, char* argv[])
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before swap.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y);
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{