hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-13 01:04:01 +0200
committerhachem <im@hachem.wtf>2026-09-13 01:04:01 +0200
commit61ef54dd242d26f3668e306750e623d337978307 (patch)
tree8c61219eac5133660f962fc43da1fdffbdd38793
parent1fa69b34a0968cf79461f37b8d9fdadba745b4d3 (diff)
chore: capture tool and dev env hooks
-rw-r--r--src/core/application.cpp58
-rw-r--r--src/main.cpp55
-rwxr-xr-xtools/capture.py108
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())