diff options
| author | hachem <im@hachem.wtf> | 2026-09-13 01:04:01 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-13 01:04:01 +0200 |
| commit | 61ef54dd242d26f3668e306750e623d337978307 (patch) | |
| tree | 8c61219eac5133660f962fc43da1fdffbdd38793 | |
| parent | 1fa69b34a0968cf79461f37b8d9fdadba745b4d3 (diff) | |
chore: capture tool and dev env hooks
| -rw-r--r-- | src/core/application.cpp | 58 | ||||
| -rw-r--r-- | src/main.cpp | 55 | ||||
| -rwxr-xr-x | tools/capture.py | 108 |
3 files changed, 221 insertions, 0 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp index d04cde8..e02edb6 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -9,7 +9,12 @@ #include <glad/gl.h> +#include <algorithm> +#include <charconv> #include <chrono> +#include <cstdlib> +#include <fstream> +#include <vector> namespace Hsdbg { @@ -19,6 +24,38 @@ namespace Hsdbg constexpr float MAX_DELTA_TIME = 0.25f; constexpr double MINIMIZED_WAIT_TIMEOUT = 0.1; + + auto capture_frame(uint64_t frame, uint32_t width, uint32_t height) -> bool + { + static const char* path = std::getenv("HSDBG_CAPTURE"); + if (path == nullptr) + return false; + + static const uint64_t target = [] { + uint64_t value = 240; + if (const char* text = std::getenv("HSDBG_CAPTURE_FRAME")) + std::from_chars(text, text + std::char_traits<char>::length(text), value); + return value; + }(); + + if (frame != target) + return false; + + const size_t stride = static_cast<size_t>(width) * 3; + std::vector<unsigned char> pixels(stride * height); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), + GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); + + std::ofstream out(path, std::ios::binary); + out << "P6\n" << width << ' ' << height << "\n255\n"; + for (uint32_t y = height; y-- > 0;) + out.write(reinterpret_cast<const char*>(pixels.data() + y * stride), + static_cast<std::streamsize>(stride)); + + Log::info("capture: wrote '{}' ({}x{})", path, width, height); + return true; + } } Application* Application::s_instance = nullptr; @@ -109,6 +146,24 @@ namespace Hsdbg auto Application::update() -> void { m_debugger->update(); + + static const std::vector<uint64_t> step_frames = [] { + std::vector<uint64_t> frames; + const char* text = std::getenv("HSDBG_STEP_AT"); + for (const char* at = text; at != nullptr && *at != '\0';) + { + uint64_t value = 0; + const auto [next, error] = std::from_chars(at, at + std::char_traits<char>::length(at), value); + if (error != std::errc{}) + break; + frames.push_back(value); + at = *next == ',' ? next + 1 : next; + } + return frames; + }(); + + if (std::ranges::find(step_frames, m_frame_count) != step_frames.end() && m_debugger->is_stopped()) + (void)m_debugger->step_over(); } auto Application::render() -> void @@ -123,5 +178,8 @@ namespace Hsdbg m_ui->begin_frame(); m_ui->draw(*m_debugger); m_ui->end_frame(); + + if (capture_frame(m_frame_count, m_window->framebuffer_width(), m_window->framebuffer_height())) + close(); } } diff --git a/src/main.cpp b/src/main.cpp index b96a74a..5af3d15 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,59 @@ #include "debugger/debugger.h" +#include <algorithm> +#include <charconv> +#include <cstdlib> +#include <filesystem> #include <span> +#include <string_view> + +namespace +{ + auto dev_window_size(Hsdbg::ApplicationSpec& spec) -> void + { + const char* text = std::getenv("HSDBG_WINDOW"); + if (text == nullptr) + return; + + const std::string_view value(text); + const size_t x = value.find('x'); + if (x == std::string_view::npos) + return; + + std::from_chars(value.data(), value.data() + x, spec.width); + std::from_chars(value.data() + x + 1, value.data() + value.size(), spec.height); + } + + auto dev_break_and_launch(Hsdbg::Debugger& debugger) -> void + { + const char* text = std::getenv("HSDBG_BREAK"); + if (text == nullptr || !debugger.has_target()) + return; + + const std::string_view value(text); + const size_t colon = value.rfind(':'); + if (colon == std::string_view::npos) + return; + + uint32_t line = 0; + std::from_chars(value.data() + colon + 1, value.data() + value.size(), line); + + const std::string_view name = value.substr(0, colon); + const std::span<const std::filesystem::path> files = debugger.source_files(); + const auto known = std::ranges::find_if(files, [&](const std::filesystem::path& candidate) + { + return candidate.filename() == name; + }); + + debugger.add_breakpoint(known != files.end() ? *known : std::filesystem::path(name), line); + + Hsdbg::LaunchSpec launch; + launch.executable = debugger.target_path(); + if (const auto result = debugger.launch(launch); !result) + Hsdbg::Log::error("{}", result.error()); + } +} auto main(int argc, char** argv) -> int { @@ -13,6 +65,7 @@ auto main(int argc, char** argv) -> int spec.name = "hsdbg"; spec.width = 1600; spec.height = 900; + dev_window_size(spec); Hsdbg::Application app(spec); @@ -20,6 +73,8 @@ auto main(int argc, char** argv) -> int { if (const auto result = app.debugger().load_target(arguments[1]); !result) Hsdbg::Log::error("{}", result.error()); + + dev_break_and_launch(app.debugger()); } app.run(); diff --git a/tools/capture.py b/tools/capture.py new file mode 100755 index 0000000..f30e9cd --- /dev/null +++ b/tools/capture.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Capture a screenshot of hsdbg without touching the running app. + +Runs the binary with the dev hooks the app reads from the environment, lets it +draw for a number of frames, and converts the framebuffer dump it writes into a +png. Useful for eyeballing ui changes from a shell (or an agent) that cannot +grab the screen. + + tools/capture.py out.png [--build build] [--target test/hello] + [--break hello.cpp:13] [--step 150,200] [--open palette] + [--frame 240] [--window 1600x900] [--cwd dir] + +Hooks (all read once at startup): + HSDBG_CAPTURE=<file.ppm> dump the frame HSDBG_CAPTURE_FRAME (default 240) and quit + HSDBG_WINDOW=<w>x<h> initial window size + HSDBG_BREAK=<file>:<line> plant a breakpoint and launch the target right away + HSDBG_STEP_AT=<f>[,<f>...] step over a line on those frames while stopped + HSDBG_OPEN=palette|target|preferences[:tab] raise that overlay on the second frame + +--cwd picks where imgui.ini / hsdbg.ini are read and written (default: a fresh +directory beside the output, so your own layout and preferences stay untouched). +""" + +import argparse +import os +import pathlib +import struct +import subprocess +import sys +import zlib + + +def ppm_to_png(src: pathlib.Path, dst: pathlib.Path) -> tuple[int, int]: + data = src.read_bytes() + magic, size, _depth, raw = data.split(b"\n", 3) + assert magic == b"P6", "expected a binary ppm" + width, height = map(int, size.split()) + stride = width * 3 + rows = b"".join(b"\x00" + raw[y * stride:(y + 1) * stride] for y in range(height)) + + def chunk(kind: bytes, body: bytes) -> bytes: + payload = kind + body + return struct.pack(">I", len(body)) + payload + struct.pack(">I", zlib.crc32(payload) & 0xFFFFFFFF) + + png = (b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(rows, 6)) + + chunk(b"IEND", b"")) + dst.write_bytes(png) + return width, height + + +def main() -> int: + root = pathlib.Path(__file__).resolve().parent.parent + + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("output", type=pathlib.Path, help="png to write") + parser.add_argument("--build", default="build", help="build directory holding the hsdbg binary") + parser.add_argument("--target", help="executable to load") + parser.add_argument("--break", dest="breakpoint", metavar="FILE:LINE", help="breakpoint to plant, then launch") + parser.add_argument("--step", metavar="FRAMES", help="comma-separated frames on which to step over") + parser.add_argument("--open", metavar="OVERLAY", + help="overlay to raise: palette, target, or preferences[:tab]") + parser.add_argument("--frame", type=int, default=240, help="frame to capture (default 240)") + parser.add_argument("--window", metavar="WxH", help="window size") + parser.add_argument("--cwd", type=pathlib.Path, help="directory to run in (imgui.ini / hsdbg.ini live there)") + args = parser.parse_args() + + binary = root / args.build / "hsdbg" + if not binary.exists(): + print(f"no binary at {binary}; build first", file=sys.stderr) + return 1 + + output = args.output.resolve() + cwd = (args.cwd or output.parent / f".{output.stem}").resolve() + cwd.mkdir(parents=True, exist_ok=True) + ppm = cwd / "capture.ppm" + + env = dict(os.environ, HSDBG_CAPTURE=str(ppm), HSDBG_CAPTURE_FRAME=str(args.frame)) + if args.breakpoint: + env["HSDBG_BREAK"] = args.breakpoint + if args.step: + env["HSDBG_STEP_AT"] = args.step + if args.open: + env["HSDBG_OPEN"] = args.open + if args.window: + env["HSDBG_WINDOW"] = args.window + + command = [str(binary)] + if args.target: + command.append(str(pathlib.Path(args.target).resolve())) + + log = cwd / "capture.log" + with log.open("w") as sink: + result = subprocess.run(command, cwd=cwd, env=env, stdout=sink, stderr=subprocess.STDOUT) + + if not ppm.exists(): + print(f"no capture written (exit {result.returncode}); see {log}", file=sys.stderr) + return 1 + + width, height = ppm_to_png(ppm, output) + ppm.unlink() + print(f"{output} ({width}x{height})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |
