From c54da47a5bde456c92a7b83575c259966bc33f9a Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 15:34:22 +0300 Subject: [PATCH] sokol: add vplayground feature to print out graphics (fixes #19857) --- vlib/gg/frame_pacing_test.v | 40 +++++++++++++++++++++++ vlib/gg/recorder.c.v | 45 ++++++++++++++++---------- vlib/gg/recorder.v | 63 +++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 16 deletions(-) diff --git a/vlib/gg/frame_pacing_test.v b/vlib/gg/frame_pacing_test.v index 2d8c637c7..a96278810 100644 --- a/vlib/gg/frame_pacing_test.v +++ b/vlib/gg/frame_pacing_test.v @@ -1,6 +1,7 @@ // vtest build: !docker-ubuntu-musl // needs GL/gl.h module gg +import encoding.base64 import time fn test_swap_interval_frame_budget() { @@ -10,3 +11,42 @@ fn test_swap_interval_frame_budget() { assert swap_interval_frame_budget(2) == time.second / 30 assert swap_interval_frame_budget(3) == time.second / 20 } + +fn test_parse_screenshot_frames_ignores_blank_entries() { + assert parse_screenshot_frames('') == []u64{} + assert parse_screenshot_frames('1, 2,,4 ') == [u64(1), 2, 4] +} + +fn test_new_gg_recorder_settings_stdout_defaults_to_first_frame() { + settings := new_gg_recorder_settings_from_env({ + 'VGG_SCREENSHOT_OUTPUT': 'stdout' + }, '/tmp/minimal_demo') + assert settings.screenshot_output == .stdout + assert settings.screenshot_frames == [u64(1)] + assert settings.stop_at_frame == 1 +} + +fn test_new_gg_recorder_settings_stdout_uses_last_frame_as_default_stop() { + settings := new_gg_recorder_settings_from_env({ + 'VGG_SCREENSHOT_OUTPUT': 'stdout' + 'VGG_SCREENSHOT_FRAMES': '2, 4' + }, '/tmp/minimal_demo') + assert settings.screenshot_frames == [u64(2), 4] + assert settings.stop_at_frame == 4 +} + +fn test_new_gg_recorder_settings_preserves_explicit_stop_frame() { + settings := new_gg_recorder_settings_from_env({ + 'VGG_SCREENSHOT_OUTPUT': 'stdout' + 'VGG_SCREENSHOT_FRAMES': '3' + 'VGG_STOP_AT_FRAME': '9' + }, '/tmp/minimal_demo') + assert settings.stop_at_frame == 9 +} + +fn test_screenshot_stdout_payload_contains_expected_marker() { + png := [u8(0x89), `P`, `N`, `G`] + encoded := base64.encode(png) + payload := screenshot_stdout_payload(7, png) + assert payload == '${gg_record_stdout_prefix} frame=7 format=png encoding=base64 data=${encoded}' +} diff --git a/vlib/gg/recorder.c.v b/vlib/gg/recorder.c.v index b73be62f0..0dbcbeeef 100644 --- a/vlib/gg/recorder.c.v +++ b/vlib/gg/recorder.c.v @@ -3,38 +3,51 @@ module gg import sokol.sapp import os -// record_frame records the current frame to a file. +fn emit_recorded_frame_to_stdout(frame u64) ! { + screenshot_file_path := os.join_path(os.vtmp_dir(), + 'vgg_record_stdout_${os.getpid()}_${frame}.png') + sapp.screenshot_png(screenshot_file_path)! + defer { + os.rm(screenshot_file_path) or {} + } + png := os.read_bytes(screenshot_file_path)! + println(screenshot_stdout_payload(frame, png)) + flush_stdout() +} + +// record_frame records the current frame to a file or stdout. // record_frame acts according to settings specified in `gg.recorder_settings`. @[if gg_record ?] pub fn (mut ctx Context) record_frame() { if ctx.frame in recorder_settings.screenshot_frames { - screenshot_file_path := '${recorder_settings.screenshot_prefix}${ctx.frame}.png' - $if gg_record_trace ? { - eprintln('>>> screenshoting ${screenshot_file_path}') + match recorder_settings.screenshot_output { + .stdout { + $if gg_record_trace ? { + eprintln('>>> screenshotting frame ${ctx.frame} to stdout') + } + emit_recorded_frame_to_stdout(ctx.frame) or { panic(err) } + } + .file { + screenshot_file_path := '${recorder_settings.screenshot_prefix}${ctx.frame}.png' + $if gg_record_trace ? { + eprintln('>>> screenshotting ${screenshot_file_path}') + } + sapp.screenshot_png(screenshot_file_path) or { panic(err) } + } } - - sapp.screenshot_png(screenshot_file_path) or { panic(err) } } if ctx.frame == recorder_settings.stop_at_frame { $if gg_record_trace ? { eprintln('>>> exiting at frame ${ctx.frame}') } + flush_stdout() exit(0) } } fn new_gg_recorder_settings() &SSRecorderSettings { $if gg_record ? { - stop_frame := os.getenv_opt('VGG_STOP_AT_FRAME') or { '-1' }.i64() - frames := os.getenv('VGG_SCREENSHOT_FRAMES').split_any(',').map(it.u64()) - folder := os.getenv('VGG_SCREENSHOT_FOLDER') - prefix := os.join_path_single(folder, os.file_name(os.executable()).all_before('.') + '_') - return &SSRecorderSettings{ - stop_at_frame: stop_frame - screenshot_frames: frames - screenshot_folder: folder - screenshot_prefix: prefix - } + return new_gg_recorder_settings_from_env(os.environ(), os.executable()) } $else { return &SSRecorderSettings{} } diff --git a/vlib/gg/recorder.v b/vlib/gg/recorder.v index 6cfa780e9..abed1c722 100644 --- a/vlib/gg/recorder.v +++ b/vlib/gg/recorder.v @@ -1,5 +1,15 @@ module gg +import encoding.base64 +import os + +enum ScreenshotOutput { + file + stdout +} + +const gg_record_stdout_prefix = '__V_GG_IMAGE__' + @[heap] pub struct SSRecorderSettings { pub mut: @@ -7,4 +17,57 @@ pub mut: screenshot_frames []u64 screenshot_folder string screenshot_prefix string + screenshot_output ScreenshotOutput = .file +} + +fn parse_screenshot_frames(value string) []u64 { + mut frames := []u64{} + for raw_frame in value.split(',') { + frame := raw_frame.trim_space() + if frame == '' { + continue + } + frames << frame.u64() + } + return frames +} + +fn parse_screenshot_output(value string) ScreenshotOutput { + return match value.trim_space().to_lower() { + 'stdout' { .stdout } + else { .file } + } +} + +fn recorder_env_value(env map[string]string, key string, fallback string) string { + value := env[key] + if value == '' { + return fallback + } + return value +} + +fn screenshot_stdout_payload(frame u64, png []u8) string { + return '${gg_record_stdout_prefix} frame=${frame} format=png encoding=base64 data=${base64.encode(png)}' +} + +fn new_gg_recorder_settings_from_env(env map[string]string, executable string) &SSRecorderSettings { + mut stop_frame := recorder_env_value(env, 'VGG_STOP_AT_FRAME', '-1').i64() + mut frames := parse_screenshot_frames(env['VGG_SCREENSHOT_FRAMES']) + folder := env['VGG_SCREENSHOT_FOLDER'] + output := parse_screenshot_output(env['VGG_SCREENSHOT_OUTPUT']) + if output == .stdout && frames.len == 0 { + frames << u64(1) + } + if output == .stdout && stop_frame < 0 && frames.len > 0 { + stop_frame = i64(frames[frames.len - 1]) + } + prefix := os.join_path_single(folder, os.file_name(executable).all_before('.') + '_') + return &SSRecorderSettings{ + stop_at_frame: stop_frame + screenshot_frames: frames + screenshot_folder: folder + screenshot_prefix: prefix + screenshot_output: output + } } -- 2.39.5