Files
Aethera/render_3D/third_party/datoviz/testing/dvz_live_canvas.c
T
2026-08-20 10:23:47 +08:00

1546 lines
43 KiB
C

/*
* Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project root for details.
* SPDX-License-Identifier: MIT
*/
/*************************************************************************************************/
/* Interactive canvas smoke app */
/*************************************************************************************************/
/*************************************************************************************************/
/* Includes */
/*************************************************************************************************/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "_alloc.h"
#include "_assertions.h"
#include "_compat.h"
#include "_log.h"
#include "_scene.h"
#include "canvas_internal.h"
#include "frame_plan/frame_plan.h"
#include "datoviz/canvas.h"
#include "datoviz/drp2.h"
#include "datoviz/input/keyboard.h"
#include "datoviz/input/keycodes.h"
#include "datoviz/scene.h"
#include "datoviz/stream.h"
#include "datoviz/vk/device.h"
#include "datoviz/vk/gpu.h"
#include "datoviz/vk/instance.h"
#include "datoviz/vk/memory.h"
#include "datoviz/vk/queues.h"
#include "datoviz/vklite/commands.h"
#include "datoviz/vklite/rendering.h"
#include "datoviz/window.h"
#include "datoviz/window/backend.h"
#if DVZ_HAS_GLFW
#define GLFW_INCLUDE_NONE
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#endif
/*************************************************************************************************/
/* Constants */
/*************************************************************************************************/
#define DVZ_CANVAS_DEFAULT_BG_R 0.08f
#define DVZ_CANVAS_DEFAULT_BG_G 0.12f
#define DVZ_CANVAS_DEFAULT_BG_B 0.16f
#define DVZ_CANVAS_DEFAULT_BG_A 1.00f
#define DVZ_CANVAS_DEFAULT_FPS 60
#define DVZ_CANVAS_DEFAULT_WIDTH 1024
#define DVZ_CANVAS_DEFAULT_HEIGHT 640
#define DVZ_CANVAS_BENCHMARK_DEFAULT_FRAMES 10000
#define DVZ_CANVAS_BENCHMARK_WARMUP_FRAMES 200
/*************************************************************************************************/
/* Structs */
/*************************************************************************************************/
typedef enum
{
DVZ_CANVAS_DRAW_CLEAR,
DVZ_CANVAS_DRAW_SCENE_DRP2,
} DvzCanvasDrawMode;
typedef struct DvzCanvasAppOptions
{
DvzBackend backend;
DvzCanvasRenderMode render_mode;
uint32_t width;
uint32_t height;
uint32_t max_frames;
float bg[4];
int fps;
VkPresentModeKHR present_mode;
double duration_s;
const char* title;
const char* record_path;
const char* screenshot_base;
DvzVideoCaptureMode record_mode;
DvzCanvasDrawMode draw_mode;
bool start_recording;
bool benchmark;
} DvzCanvasAppOptions;
typedef struct DvzCanvasApp
{
DvzCanvasAppOptions options;
DvzInstance* instance;
DvzWindowHost* host;
DvzDevice* device;
DvzVma* allocator;
DvzWindow* window;
DvzCanvas* canvas;
DvzDrp2Runtime* drp2_runtime;
DvzFramePlanEmitter* scene_emitter;
DvzCapabilitySnapshot scene_caps;
DvzFramePlanEmitConfig scene_emit_cfg;
DvzVideoSinkConfig video_cfg;
bool running;
bool recording;
bool toggle_record_requested;
bool screenshot_requested;
uint32_t screenshot_index;
int key_prev_escape;
int key_prev_s;
int key_prev_r;
DvzCallbackId keyboard_subscription_id;
uint64_t scene_frame_index;
bool scene_last_ok;
bool scene_reported_ok;
bool scene_reported_error;
bool present_mode_reported;
double* benchmark_frame_ms;
uint32_t benchmark_warmup_frames;
uint32_t benchmark_sample_count;
double benchmark_start_s;
double benchmark_last_s;
} DvzCanvasApp;
/*************************************************************************************************/
/* Helpers */
/*************************************************************************************************/
/**
* Fill application options with sensible defaults.
*
* @param options destination options structure
*/
static void _dvz_canvas_options_default(DvzCanvasAppOptions* options)
{
ANN(options);
options->backend = DVZ_BACKEND_GLFW;
options->render_mode = DVZ_CANVAS_RENDER_MODE_PRESENT;
options->width = DVZ_CANVAS_DEFAULT_WIDTH;
options->height = DVZ_CANVAS_DEFAULT_HEIGHT;
options->max_frames = 0;
options->bg[0] = DVZ_CANVAS_DEFAULT_BG_R;
options->bg[1] = DVZ_CANVAS_DEFAULT_BG_G;
options->bg[2] = DVZ_CANVAS_DEFAULT_BG_B;
options->bg[3] = DVZ_CANVAS_DEFAULT_BG_A;
options->fps = DVZ_CANVAS_DEFAULT_FPS;
options->present_mode = VK_PRESENT_MODE_FIFO_KHR;
options->duration_s = 0.0;
options->title = "Datoviz Canvas";
options->record_path = "canvas.mp4";
options->screenshot_base = "canvas_capture";
options->record_mode = DVZ_VIDEO_CAPTURE_AUTO;
options->draw_mode = DVZ_CANVAS_DRAW_CLEAR;
options->start_recording = false;
options->benchmark = false;
}
/**
* Parse an unsigned integer command-line value.
*
* @param arg input text
* @param out destination parsed value
* @returns true on success, false on parse failure
*/
static bool _dvz_canvas_parse_u32(const char* arg, uint32_t* out)
{
ANN(arg);
ANN(out);
char* end = NULL;
unsigned long value = strtoul(arg, &end, 10);
if (end == arg || *end != '\0' || value == 0 || value > UINT32_MAX)
{
return false;
}
*out = (uint32_t)value;
return true;
}
/**
* Parse a double command-line value.
*
* @param arg input text
* @param out destination parsed value
* @returns true on success, false on parse failure
*/
static bool _dvz_canvas_parse_double(const char* arg, double* out)
{
ANN(arg);
ANN(out);
char* end = NULL;
double value = strtod(arg, &end);
if (end == arg || *end != '\0' || value < 0.0)
{
return false;
}
*out = value;
return true;
}
/**
* Parse an RGBA color list encoded as r,g,b,a floats.
*
* @param arg input text
* @param rgba destination color values
* @returns true on success, false otherwise
*/
static bool _dvz_canvas_parse_bg(const char* arg, float rgba[4])
{
ANN(arg);
ANN(rgba);
float r = 0, g = 0, b = 0, a = 0;
if (sscanf(arg, "%f,%f,%f,%f", &r, &g, &b, &a) != 4)
{
return false;
}
rgba[0] = r;
rgba[1] = g;
rgba[2] = b;
rgba[3] = a;
return true;
}
/**
* Return a display name for a Vulkan present mode.
*
* @param mode Vulkan present mode
* @returns static present mode name
*/
static const char* _dvz_canvas_present_mode_name(VkPresentModeKHR mode)
{
switch (mode)
{
case VK_PRESENT_MODE_IMMEDIATE_KHR:
return "immediate";
case VK_PRESENT_MODE_MAILBOX_KHR:
return "mailbox";
case VK_PRESENT_MODE_FIFO_KHR:
return "fifo";
case VK_PRESENT_MODE_FIFO_RELAXED_KHR:
return "fifo-relaxed";
default:
return "unknown";
}
}
/**
* Return a high-resolution monotonic timestamp in seconds.
*
* @returns monotonic timestamp in seconds
*/
static double _dvz_canvas_benchmark_now(void)
{
struct timespec ts = {0};
#if defined(CLOCK_MONOTONIC)
clock_gettime(CLOCK_MONOTONIC, &ts);
#else
timespec_get(&ts, TIME_UTC);
#endif
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
/**
* Compare two double values for qsort().
*
* @param a first value pointer
* @param b second value pointer
* @returns negative, zero, or positive comparison result
*/
static int _dvz_canvas_compare_double(const void* a, const void* b)
{
const double da = *(const double*)a;
const double db = *(const double*)b;
if (da < db)
{
return -1;
}
if (da > db)
{
return 1;
}
return 0;
}
/**
* Return an integer percentile from a sorted frame-time array.
*
* @param sorted sorted frame-time samples in milliseconds
* @param count number of samples
* @param percentile percentile in the [0, 100] range
* @returns selected percentile value, or zero without samples
*/
static double
_dvz_canvas_benchmark_percentile(const double* sorted, uint32_t count, uint32_t percentile)
{
if (sorted == NULL || count == 0)
{
return 0.0;
}
if (percentile >= 100)
{
return sorted[count - 1];
}
uint64_t numerator = (uint64_t)percentile * (uint64_t)(count - 1) + 50;
uint32_t idx = (uint32_t)(numerator / 100);
if (idx >= count)
{
idx = count - 1;
}
return sorted[idx];
}
/**
* Initialize benchmark state before entering the frame loop.
*
* @param app canvas app state
* @returns true on success, false on allocation failure
*/
static bool _dvz_canvas_benchmark_begin(DvzCanvasApp* app)
{
ANN(app);
if (!app->options.benchmark)
{
return true;
}
uint32_t max_frames = app->options.max_frames;
if (max_frames == 0)
{
max_frames = DVZ_CANVAS_BENCHMARK_DEFAULT_FRAMES;
app->options.max_frames = max_frames;
}
uint32_t max_warmup = max_frames / 10;
app->benchmark_warmup_frames = DVZ_CANVAS_BENCHMARK_WARMUP_FRAMES;
if (app->benchmark_warmup_frames > max_warmup)
{
app->benchmark_warmup_frames = max_warmup;
}
uint32_t sample_capacity = max_frames - app->benchmark_warmup_frames;
app->benchmark_frame_ms = (double*)dvz_calloc(sample_capacity, sizeof(double));
if (app->benchmark_frame_ms == NULL)
{
dvz_fprintf(stderr, "benchmark: failed to allocate frame-time buffer\n");
return false;
}
app->benchmark_sample_count = 0;
double now = _dvz_canvas_benchmark_now();
app->benchmark_start_s = now;
app->benchmark_last_s = now;
return true;
}
/**
* Record one submitted frame in the active benchmark.
*
* @param app canvas app state
* @param submitted_frames submitted frame count after the current submit
*/
static void _dvz_canvas_benchmark_record(DvzCanvasApp* app, uint64_t submitted_frames)
{
ANN(app);
if (!app->options.benchmark)
{
return;
}
double now = _dvz_canvas_benchmark_now();
if (submitted_frames == app->benchmark_warmup_frames)
{
app->benchmark_start_s = now;
app->benchmark_last_s = now;
return;
}
if (submitted_frames < app->benchmark_warmup_frames)
{
return;
}
uint32_t capacity = app->options.max_frames - app->benchmark_warmup_frames;
if (app->benchmark_sample_count >= capacity)
{
return;
}
app->benchmark_frame_ms[app->benchmark_sample_count++] =
(now - app->benchmark_last_s) * 1000.0;
app->benchmark_last_s = now;
}
/**
* Print benchmark throughput, frame-time distribution, and stutter counts.
*
* @param app canvas app state
*/
static void _dvz_canvas_benchmark_end(DvzCanvasApp* app)
{
ANN(app);
if (!app->options.benchmark || app->benchmark_sample_count == 0)
{
return;
}
uint32_t count = app->benchmark_sample_count;
double* sorted = (double*)dvz_calloc(count, sizeof(double));
if (sorted == NULL)
{
dvz_fprintf(stderr, "benchmark: failed to allocate percentile buffer\n");
return;
}
dvz_memcpy(sorted, count * sizeof(double), app->benchmark_frame_ms, count * sizeof(double));
qsort(sorted, count, sizeof(double), _dvz_canvas_compare_double);
double sum_ms = 0.0;
double max_ms = sorted[count - 1];
uint32_t stutter_2ms = 0;
uint32_t stutter_5ms = 0;
uint32_t stutter_10ms = 0;
uint32_t stutter_16ms = 0;
for (uint32_t i = 0; i < count; i++)
{
double dt = app->benchmark_frame_ms[i];
sum_ms += dt;
if (dt > 2.0)
{
stutter_2ms++;
}
if (dt > 5.0)
{
stutter_5ms++;
}
if (dt > 10.0)
{
stutter_10ms++;
}
if (dt > 16.6667)
{
stutter_16ms++;
}
}
double elapsed_s = app->benchmark_last_s - app->benchmark_start_s;
double fps = elapsed_s > 0.0 ? (double)count / elapsed_s : 0.0;
double avg_ms = count > 0 ? sum_ms / (double)count : 0.0;
VkPresentModeKHR resolved = VK_PRESENT_MODE_FIFO_KHR;
bool has_resolved = dvz_canvas_swapchain_present_mode(app->canvas, &resolved);
dvz_fprintf(
stderr,
"benchmark: frames=%u warmup=%u samples=%u elapsed=%.6fs fps=%.2f avg_ms=%.4f\n",
app->options.max_frames, app->benchmark_warmup_frames, count, elapsed_s, fps, avg_ms);
if (has_resolved)
{
dvz_fprintf(
stderr, "benchmark: present requested=%s (%d), resolved=%s (%d)\n",
_dvz_canvas_present_mode_name(app->options.present_mode),
(int)app->options.present_mode, _dvz_canvas_present_mode_name(resolved),
(int)resolved);
}
dvz_fprintf(
stderr,
"benchmark: frame_ms min=%.4f p50=%.4f p90=%.4f p95=%.4f p99=%.4f max=%.4f\n",
sorted[0], _dvz_canvas_benchmark_percentile(sorted, count, 50),
_dvz_canvas_benchmark_percentile(sorted, count, 90),
_dvz_canvas_benchmark_percentile(sorted, count, 95),
_dvz_canvas_benchmark_percentile(sorted, count, 99), max_ms);
dvz_fprintf(
stderr, "benchmark: stutters >2ms=%u >5ms=%u >10ms=%u >16.67ms=%u\n", stutter_2ms,
stutter_5ms, stutter_10ms, stutter_16ms);
dvz_free(sorted);
}
/**
* Print the requested and resolved swapchain present modes once they are available.
*
* @param app canvas app state
*/
static void _dvz_canvas_report_present_mode(DvzCanvasApp* app)
{
ANN(app);
if (
app->present_mode_reported ||
app->options.render_mode != DVZ_CANVAS_RENDER_MODE_PRESENT || app->canvas == NULL)
{
return;
}
VkPresentModeKHR resolved = VK_PRESENT_MODE_FIFO_KHR;
if (!dvz_canvas_swapchain_present_mode(app->canvas, &resolved))
{
return;
}
VkPresentModeKHR requested = app->options.present_mode;
dvz_fprintf(
stderr, "present mode: requested=%s (%d), resolved=%s (%d)\n",
_dvz_canvas_present_mode_name(requested), (int)requested,
_dvz_canvas_present_mode_name(resolved), (int)resolved);
app->present_mode_reported = true;
}
/**
* Print command-line usage information.
*/
static void _dvz_canvas_usage(void)
{
dvz_fprintf(
stderr,
"usage: dvz_live_canvas [--backend glfw|offscreen] [--width N] [--height N]\n"
" [--mode present|offscreen] [--frames N] [--bg r,g,b,a] [--fps N]\n"
" [--draw clear|scene-drp2]\n"
" [--present fifo|immediate] [--duration seconds]\n"
" [--record path.mp4] [--record-mode auto|external|cpu]\n"
" [--start-recording] [--screenshots base] [--benchmark]\n"
"\n"
"hotkeys: Esc quit, S screenshot, R toggle recording\n");
}
/**
* Parse command-line arguments into options.
*
* @param argc argument count
* @param argv argument vector
* @param options destination options
* @returns true on success, false on invalid arguments
*/
static bool _dvz_canvas_parse_args(int argc, char** argv, DvzCanvasAppOptions* options)
{
ANN(options);
bool mode_explicit = false;
bool backend_explicit = false;
bool present_explicit = false;
for (int i = 1; i < argc; ++i)
{
const char* arg = argv[i];
if (strcmp(arg, "--") == 0)
{
continue;
}
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0)
{
_dvz_canvas_usage();
return false;
}
if (strcmp(arg, "--start-recording") == 0)
{
options->start_recording = true;
continue;
}
if (strcmp(arg, "--benchmark") == 0)
{
options->benchmark = true;
continue;
}
if (i + 1 >= argc)
{
dvz_fprintf(stderr, "missing value after %s\\n", arg);
return false;
}
const char* value = argv[++i];
if (strcmp(arg, "--width") == 0)
{
if (!_dvz_canvas_parse_u32(value, &options->width))
return false;
}
else if (strcmp(arg, "--backend") == 0)
{
backend_explicit = true;
if (strcmp(value, "glfw") == 0)
{
options->backend = DVZ_BACKEND_GLFW;
}
else if (strcmp(value, "offscreen") == 0)
{
options->backend = DVZ_BACKEND_OFFSCREEN;
}
else
{
dvz_fprintf(stderr, "invalid backend: %s\\n", value);
return false;
}
}
else if (strcmp(arg, "--mode") == 0)
{
mode_explicit = true;
if (strcmp(value, "present") == 0)
{
options->render_mode = DVZ_CANVAS_RENDER_MODE_PRESENT;
}
else if (strcmp(value, "offscreen") == 0)
{
options->render_mode = DVZ_CANVAS_RENDER_MODE_OFFSCREEN;
}
else
{
dvz_fprintf(stderr, "invalid mode: %s\\n", value);
return false;
}
}
else if (strcmp(arg, "--height") == 0)
{
if (!_dvz_canvas_parse_u32(value, &options->height))
return false;
}
else if (strcmp(arg, "--frames") == 0)
{
if (!_dvz_canvas_parse_u32(value, &options->max_frames))
return false;
}
else if (strcmp(arg, "--bg") == 0)
{
if (!_dvz_canvas_parse_bg(value, options->bg))
return false;
}
else if (strcmp(arg, "--fps") == 0)
{
uint32_t fps = 0;
if (!_dvz_canvas_parse_u32(value, &fps))
return false;
options->fps = (int)fps;
}
else if (strcmp(arg, "--draw") == 0)
{
if (strcmp(value, "clear") == 0)
{
options->draw_mode = DVZ_CANVAS_DRAW_CLEAR;
}
else if (strcmp(value, "scene-drp2") == 0)
{
options->draw_mode = DVZ_CANVAS_DRAW_SCENE_DRP2;
}
else
{
dvz_fprintf(stderr, "invalid draw mode: %s\\n", value);
return false;
}
}
else if (strcmp(arg, "--duration") == 0)
{
if (!_dvz_canvas_parse_double(value, &options->duration_s))
return false;
}
else if (strcmp(arg, "--record") == 0)
{
options->record_path = value;
}
else if (strcmp(arg, "--screenshots") == 0)
{
options->screenshot_base = value;
}
else if (strcmp(arg, "--record-mode") == 0)
{
if (strcmp(value, "auto") == 0)
{
options->record_mode = DVZ_VIDEO_CAPTURE_AUTO;
}
else if (strcmp(value, "external") == 0)
{
options->record_mode = DVZ_VIDEO_CAPTURE_EXTERNAL;
}
else if (strcmp(value, "cpu") == 0)
{
options->record_mode = DVZ_VIDEO_CAPTURE_CPU_READBACK;
}
else
{
dvz_fprintf(stderr, "invalid record mode: %s\\n", value);
return false;
}
}
else if (strcmp(arg, "--present") == 0)
{
present_explicit = true;
if (strcmp(value, "immediate") == 0)
{
options->present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
}
else if (strcmp(value, "fifo") == 0)
{
options->present_mode = VK_PRESENT_MODE_FIFO_KHR;
}
else
{
dvz_fprintf(stderr, "invalid present mode: %s\\n", value);
return false;
}
}
else
{
dvz_fprintf(stderr, "unknown argument: %s\\n", arg);
return false;
}
}
if (!mode_explicit && backend_explicit)
{
options->render_mode = options->backend == DVZ_BACKEND_OFFSCREEN
? DVZ_CANVAS_RENDER_MODE_OFFSCREEN
: DVZ_CANVAS_RENDER_MODE_PRESENT;
}
if (options->backend == DVZ_BACKEND_OFFSCREEN &&
options->render_mode == DVZ_CANVAS_RENDER_MODE_PRESENT)
{
dvz_fprintf(
stderr, "invalid combination: --backend offscreen requires --mode offscreen\\n");
return false;
}
if (options->benchmark)
{
if (options->max_frames == 0)
{
options->max_frames = DVZ_CANVAS_BENCHMARK_DEFAULT_FRAMES;
}
if (!present_explicit && options->render_mode == DVZ_CANVAS_RENDER_MODE_PRESENT)
{
options->present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
}
}
return true;
}
/**
* Record a fullscreen clear pass for the current canvas command buffer.
*
* @param canvas owning canvas
* @param frame stream frame with command buffer and target image view
* @param user_data pointer to the RGBA clear color array
*/
static void _dvz_canvas_draw(DvzCanvas* canvas, const DvzStreamFrame* frame, void* user_data)
{
ANN(canvas);
ANN(frame);
if (frame->command_buffer == VK_NULL_HANDLE || frame->image_view == VK_NULL_HANDLE)
{
return;
}
float* bg = (float*)user_data;
ANN(bg);
DvzDevice* device = dvz_stream_device(dvz_canvas_stream(canvas));
ANN(device);
DvzCommands* cmds = dvz_commands_create_wrapper();
ANN(cmds);
dvz_commands_wrap(device, frame->command_buffer, cmds);
DvzRendering* rendering = dvz_rendering_create_wrapper();
ANN(rendering);
dvz_cmd_rendering_default(
cmds, frame->image_view, frame->extent.width, frame->extent.height,
(VkClearValue){.color.float32 = {bg[0], bg[1], bg[2], bg[3]}}, rendering);
dvz_cmd_rendering_begin(cmds, rendering);
dvz_cmd_rendering_end(cmds);
dvz_rendering_free(rendering);
dvz_commands_free(cmds);
}
/**
* Render a minimal scene through DRP2 into the current canvas frame.
*
* @param canvas owning canvas
* @param frame stream frame with command buffer and target image view
* @param user_data app state pointer
*/
static void _dvz_canvas_draw_scene_drp2(
DvzCanvas* canvas, const DvzStreamFrame* frame, void* user_data)
{
(void)canvas;
ANN(frame);
DvzCanvasApp* app = (DvzCanvasApp*)user_data;
ANN(app);
app->scene_last_ok = false;
if (app->drp2_runtime == NULL || app->scene_emitter == NULL)
{
return;
}
DvzFramePlan* plan = dvz_frame_plan("live.canvas.scene", app->scene_frame_index++);
if (plan == NULL)
{
return;
}
const char* stage = "attach frame target";
DvzDrp2ValidationResult result = {0};
static const float live_point_position[3] = {0.0f, 0.0f, 0.0f};
static const DvzColor live_point_color[1] = {{255, 96, 32, 255}};
static const float live_point_size[1] = {12.0f};
bool ok = dvz_drp2_runtime_attach_frame_target(app->drp2_runtime, 1, frame);
ok = ok && dvz_frame_plan_upload_bytes(
plan, "visual.point.0_position", 0, sizeof(live_point_position), "position",
live_point_position);
ok = ok && dvz_frame_plan_upload_bytes(
plan, "visual.point.0_color", 0, sizeof(live_point_color), "color",
live_point_color);
ok = ok && dvz_frame_plan_upload_bytes(
plan, "visual.point.0_size", 0, sizeof(live_point_size), "size",
live_point_size);
ok = ok && dvz_frame_plan_render(plan, "panel.0", "target.canvas.color", false);
ok = ok && dvz_frame_plan_render_visual(plan, "visual.point.0");
DvzFramePlanVisualMeta metadata = {0};
metadata.visual_type = DVZ_VISUAL_TYPE_POINT;
metadata.renderable_kind = DVZ_RENDERABLE_POINT_LIKE;
metadata.vertex_count = 1;
metadata.alpha_mode = DVZ_ALPHA_OPAQUE;
metadata.depth_test_enabled = true;
dvz_strlcpy(metadata.position_id, "visual.point.0_position", sizeof(metadata.position_id));
dvz_strlcpy(metadata.color_id, "visual.point.0_color", sizeof(metadata.color_id));
dvz_strlcpy(metadata.size_id, "visual.point.0_size", sizeof(metadata.size_id));
ok = ok && dvz_frame_plan_render_visual_metadata(plan, &metadata);
DvzDiagnosticReport report = {0};
dvz_diagnostic_report_init(&report);
DvzDrp2CommandStream* stream = NULL;
if (ok)
{
stage = "emit DRP2 stream";
stream = dvz_frame_plan_emitter_emit_drp2(
app->scene_emitter, plan, &app->scene_caps, &report, &app->scene_emit_cfg);
ok = stream != NULL && dvz_diagnostic_report_count(&report) == 0;
}
if (ok)
{
stage = "execute DRP2 stream";
result = dvz_drp2_runtime_execute(app->drp2_runtime, stream);
ok = result.ok && result.code == DVZ_DRP2_VALIDATION_OK;
}
app->scene_last_ok = ok;
if (ok && !app->scene_reported_ok)
{
dvz_fprintf(
stderr, "scene-drp2: rendering into canvas target %ux%u\n", frame->extent.width,
frame->extent.height);
app->scene_reported_ok = true;
}
if (!ok && !app->scene_reported_error)
{
dvz_fprintf(
stderr,
"scene-drp2: failed to %s (diagnostics=%u, validation=%d at command %u)\n", stage,
dvz_diagnostic_report_count(&report), (int)result.code, result.command_index);
app->scene_reported_error = true;
}
dvz_drp2_stream_destroy(stream);
dvz_frame_plan_destroy(plan);
}
/**
* Build a numbered PNG path for screenshot capture.
*
* @param app canvas app state
* @param out_path destination buffer
* @param out_len destination buffer length
*/
static void _dvz_canvas_screenshot_path(DvzCanvasApp* app, char* out_path, size_t out_len)
{
ANN(app);
ANN(out_path);
ASSERT(out_len > 0);
const char* base =
app->options.screenshot_base ? app->options.screenshot_base : "canvas_capture";
int n = dvz_snprintf(out_path, out_len, "%s_%04u.png", base, app->screenshot_index++);
if (n < 0 || (size_t)n >= out_len)
{
dvz_snprintf(out_path, out_len, "canvas_capture_%04u.png", app->screenshot_index++);
}
}
/**
* Toggle video recording by enabling or disabling the canvas video sink.
*
* @param app canvas app state
*/
static void _dvz_canvas_toggle_recording(DvzCanvasApp* app)
{
ANN(app);
ANN(app->canvas);
if (!app->recording)
{
const DvzWindowSurface* surface = dvz_window_surface(app->window);
ANN(surface);
app->video_cfg = dvz_video_sink_config();
app->video_cfg.encoder.backend = "auto";
app->video_cfg.encoder.width = surface->extent.width;
app->video_cfg.encoder.height = surface->extent.height;
app->video_cfg.encoder.fps = (uint32_t)app->options.fps;
app->video_cfg.encoder.mp4_path = app->options.record_path;
app->video_cfg.bitstream = NULL;
app->video_cfg.capture_mode = app->options.record_mode;
int rc = dvz_canvas_configure_video_sink(app->canvas, true, &app->video_cfg);
if (rc == 0)
{
app->recording = true;
dvz_fprintf(stderr, "recording enabled: %s\\n", app->options.record_path);
}
else
{
dvz_fprintf(stderr, "failed to enable recording\\n");
}
}
else
{
int rc = dvz_canvas_configure_video_sink(app->canvas, false, NULL);
if (rc == 0)
{
app->recording = false;
dvz_fprintf(stderr, "recording disabled\\n");
}
else
{
dvz_fprintf(stderr, "failed to disable recording\\n");
}
}
}
/**
* Handle keyboard events for quit/screenshot/record controls.
*
* @param router input router emitting the event
* @param event keyboard event
* @param user_data app state pointer
*/
static void _dvz_canvas_keyboard(
DvzInputRouter* router, const DvzKeyboardEvent* event, void* user_data)
{
ANN(router);
ANN(event);
DvzCanvasApp* app = (DvzCanvasApp*)user_data;
ANN(app);
if (event->type != DVZ_KEYBOARD_EVENT_PRESS)
{
return;
}
if (event->key == DVZ_KEY_ESCAPE)
{
app->running = false;
}
else if (event->key == DVZ_KEY_S)
{
app->screenshot_requested = true;
}
else if (event->key == DVZ_KEY_R)
{
app->toggle_record_requested = true;
}
}
/**
* Handle GLFW key presses directly for interactive shortcuts.
*
* @param handle native GLFW window handle
* @param key GLFW key code
* @param scancode platform scancode
* @param action GLFW key action
* @param mods GLFW modifiers bitmask
*/
static void _dvz_canvas_glfw_key_callback(
GLFWwindow* handle, int key, int scancode, int action, int mods)
{
(void)scancode;
(void)mods;
if (action != GLFW_PRESS)
{
return;
}
DvzWindow* window = (DvzWindow*)glfwGetWindowUserPointer(handle);
if (window == NULL)
{
return;
}
DvzCanvasApp* app = (DvzCanvasApp*)dvz_window_user_data(window);
if (app == NULL)
{
return;
}
if (key == GLFW_KEY_ESCAPE)
{
app->key_prev_escape = GLFW_PRESS;
app->running = false;
}
else if (key == GLFW_KEY_S)
{
app->key_prev_s = GLFW_PRESS;
app->screenshot_requested = true;
}
else if (key == GLFW_KEY_R)
{
app->key_prev_r = GLFW_PRESS;
app->toggle_record_requested = true;
}
}
/**
* Check whether the native GLFW window was asked to close.
*
* @param app app state pointer
* @returns true when the close button was pressed, false otherwise
*/
static bool _dvz_canvas_window_should_close(const DvzCanvasApp* app)
{
ANN(app);
if (app->window == NULL)
{
return false;
}
#if DVZ_HAS_GLFW
GLFWwindow* handle = (GLFWwindow*)dvz_window_backend_handle(app->window);
return (handle != NULL) ? glfwWindowShouldClose(handle) != 0 : false;
#else
return false;
#endif
}
/**
* Return true only on a GLFW press edge for a single key.
*
* @param current current GLFW key state
* @param previous previous GLFW key state storage
* @returns true when the key transitioned to pressed
*/
static bool _dvz_canvas_key_pressed_edge(int current, int* previous)
{
ANN(previous);
bool pressed = (current == GLFW_PRESS) && (*previous != GLFW_PRESS);
*previous = current;
return pressed;
}
/**
* Poll GLFW keyboard state and trigger app shortcuts.
*
* @param app app state pointer
*/
static void _dvz_canvas_poll_keyboard_shortcuts(DvzCanvasApp* app)
{
ANN(app);
if (app->window == NULL)
{
return;
}
#if DVZ_HAS_GLFW
GLFWwindow* handle = (GLFWwindow*)dvz_window_backend_handle(app->window);
if (handle == NULL)
{
return;
}
if (glfwGetWindowAttrib(handle, GLFW_FOCUSED) == GLFW_FALSE)
{
return;
}
if (_dvz_canvas_key_pressed_edge(glfwGetKey(handle, GLFW_KEY_ESCAPE), &app->key_prev_escape))
{
app->running = false;
}
if (_dvz_canvas_key_pressed_edge(glfwGetKey(handle, GLFW_KEY_S), &app->key_prev_s))
{
app->screenshot_requested = true;
}
if (_dvz_canvas_key_pressed_edge(glfwGetKey(handle, GLFW_KEY_R), &app->key_prev_r))
{
app->toggle_record_requested = true;
}
#endif
}
/**
* Create the Vulkan/window/canvas runtime objects needed by the app.
*
* @param app app state to initialize
* @returns true on success, false on failure
*/
static bool _dvz_canvas_init(DvzCanvasApp* app)
{
ANN(app);
DvzCanvasAppOptions options = app->options;
dvz_memset(app, sizeof(*app), 0, sizeof(*app));
app->options = options;
app->running = true;
app->host = dvz_window_host();
if (app->host == NULL)
{
dvz_fprintf(stderr, "failed to create window host\\n");
return false;
}
if (app->options.backend == DVZ_BACKEND_GLFW && !dvz_window_glfw_init())
{
dvz_fprintf(stderr, "unable to initialize GLFW\\n");
return false;
}
DvzInstanceConfig icfg = dvz_instance_config();
#if ENABLE_VALIDATION_LAYERS
icfg.flags = DVZ_INSTANCE_VALIDATION_FLAGS;
#endif
uint32_t ext_count =
dvz_window_host_required_extension_count(app->host, app->options.backend);
if (ext_count > 0)
{
const char** extensions = dvz_calloc(ext_count, sizeof(char*));
if (extensions == NULL)
{
dvz_fprintf(stderr, "failed to allocate backend extension list\\n");
return false;
}
int written = dvz_window_host_required_extensions(
app->host, app->options.backend, ext_count, extensions);
if (written != (int)ext_count)
{
dvz_fprintf(stderr, "failed to query required backend extensions\\n");
dvz_free((void*)extensions);
return false;
}
for (uint32_t i = 0; i < ext_count; i++)
{
dvz_instance_config_request_extension(&icfg, extensions[i]);
}
dvz_free((void*)extensions);
}
else if (app->options.backend != DVZ_BACKEND_OFFSCREEN)
{
dvz_fprintf(stderr, "requested backend exposes no Vulkan instance extensions\\n");
return false;
}
app->instance = dvz_instance_create(&icfg);
if (app->instance == NULL)
{
dvz_fprintf(stderr, "failed to create Vulkan instance\\n");
return false;
}
uint32_t gpu_count = dvz_instance_gpu_count(app->instance);
if (gpu_count == 0)
{
dvz_fprintf(stderr, "no Vulkan GPU available\\n");
return false;
}
DvzQueueCaps caps = {0};
if (!dvz_instance_gpu_queue_caps(app->instance, 0, &caps))
{
dvz_fprintf(stderr, "failed to query Vulkan queue capabilities\\n");
return false;
}
DvzQueues queues = {0};
dvz_queues(&caps, &queues);
DvzDeviceConfig dcfg = dvz_device_config(app->instance);
dvz_device_config_set_gpu_index(&dcfg, 0);
for (uint32_t i = 0; i < queues.queue_count; i++)
{
DvzQueue* queue = &queues.queues[i];
dvz_device_config_request_queue(&dcfg, queue->family_idx, 1);
}
VkPhysicalDeviceVulkan12Features fet12 = {0};
fet12.timelineSemaphore = true;
dvz_device_config_set_features12(&dcfg, &fet12);
VkPhysicalDeviceVulkan13Features fet13 = {0};
fet13.synchronization2 = true;
fet13.dynamicRendering = true;
dvz_device_config_set_features13(&dcfg, &fet13);
if (app->options.backend == DVZ_BACKEND_GLFW)
{
dvz_device_config_enable_canvas_extensions(&dcfg, true);
}
app->device = dvz_device_create(&dcfg);
if (app->device == NULL)
{
dvz_fprintf(stderr, "failed to create Vulkan device\\n");
return false;
}
app->allocator = dvz_allocator_create();
if (app->allocator == NULL || dvz_device_allocator(app->device, 0, app->allocator) != 0)
{
dvz_fprintf(stderr, "failed to create Vulkan allocator\\n");
return false;
}
DvzWindowConfig wcfg = dvz_window_config();
wcfg.width = app->options.width;
wcfg.height = app->options.height;
wcfg.title = app->options.title;
app->window = dvz_window_create(app->host, app->options.backend, &wcfg);
if (app->window == NULL || dvz_window_backend_type(app->window) != app->options.backend)
{
dvz_fprintf(stderr, "failed to create requested window backend\\n");
return false;
}
#if DVZ_HAS_GLFW
if (app->options.backend == DVZ_BACKEND_GLFW)
{
GLFWwindow* handle = (GLFWwindow*)dvz_window_backend_handle(app->window);
dvz_window_set_user_data(app->window, app);
if (handle != NULL)
{
glfwShowWindow(handle);
#ifdef GLFW_FOCUS_ON_SHOW
glfwSetWindowAttrib(handle, GLFW_FOCUS_ON_SHOW, GLFW_TRUE);
#endif
glfwSetKeyCallback(handle, _dvz_canvas_glfw_key_callback);
glfwFocusWindow(handle);
if (glfwGetWindowAttrib(handle, GLFW_FOCUSED) == GLFW_FALSE)
{
glfwRequestWindowAttention(handle);
}
}
}
#endif
if (
app->options.backend == DVZ_BACKEND_OFFSCREEN && app->options.duration_s <= 0.0 &&
app->options.max_frames == 0)
{
app->options.duration_s = 5.0;
dvz_fprintf(stderr, "offscreen backend selected, defaulting duration to %.1fs\\n", 5.0);
}
DvzCanvasConfig ccfg = dvz_canvas_config();
ccfg.window = app->window;
ccfg.device = app->device;
ccfg.render_mode = app->options.render_mode;
ccfg.present_mode = app->options.present_mode;
ccfg.enable_video_sink = false;
app->canvas = dvz_canvas_create(&ccfg);
if (app->canvas == NULL)
{
dvz_fprintf(stderr, "failed to create canvas\\n");
return false;
}
if (app->options.draw_mode == DVZ_CANVAS_DRAW_SCENE_DRP2)
{
DvzDrp2RuntimeConfig runtime_cfg =
dvz_drp2_runtime_vklite_config(app->device, app->allocator);
app->drp2_runtime = dvz_drp2_runtime_vklite(&runtime_cfg);
app->scene_emitter = dvz_frame_plan_emitter();
if (app->drp2_runtime == NULL || app->scene_emitter == NULL)
{
dvz_fprintf(stderr, "failed to create scene/DRP2 runtime\\n");
return false;
}
app->scene_caps = dvz_capability_snapshot();
app->scene_emit_cfg = dvz_frame_plan_emit_config();
app->scene_emit_cfg.shader_format = DVZ_SCENE_SHADER_FORMAT_GLSL;
app->scene_emit_cfg.external_color_target = true;
app->scene_emit_cfg.color_target_id = 1;
dvz_canvas_set_draw_callback(app->canvas, _dvz_canvas_draw_scene_drp2, app);
}
else
{
dvz_canvas_set_draw_callback(app->canvas, _dvz_canvas_draw, app->options.bg);
}
DvzInputRouter* router = dvz_canvas_input(app->canvas);
if (router != NULL)
{
app->keyboard_subscription_id =
dvz_input_subscribe_keyboard(router, _dvz_canvas_keyboard, app);
}
return true;
}
/**
* Destroy all runtime objects owned by the app.
*
* @param app app state to cleanup
*/
static void _dvz_canvas_destroy(DvzCanvasApp* app)
{
if (app == NULL)
{
return;
}
if (app->benchmark_frame_ms != NULL)
{
dvz_free(app->benchmark_frame_ms);
app->benchmark_frame_ms = NULL;
}
if (app->canvas != NULL)
{
DvzInputRouter* router = dvz_canvas_input(app->canvas);
if (router != NULL)
{
dvz_input_unsubscribe(router, app->keyboard_subscription_id);
app->keyboard_subscription_id = DVZ_CALLBACK_ID_NONE;
}
dvz_canvas_set_draw_callback(app->canvas, NULL, NULL);
}
if (app->scene_emitter != NULL)
{
dvz_frame_plan_emitter_destroy(app->scene_emitter);
app->scene_emitter = NULL;
}
if (app->drp2_runtime != NULL)
{
dvz_drp2_runtime_destroy(app->drp2_runtime);
app->drp2_runtime = NULL;
}
if (app->canvas != NULL)
{
dvz_canvas_destroy(app->canvas);
app->canvas = NULL;
}
if (app->window != NULL)
{
dvz_window_destroy(app->window);
app->window = NULL;
}
if (app->host != NULL)
{
dvz_window_host_destroy(app->host);
app->host = NULL;
}
if (app->device != NULL)
{
if (app->allocator != NULL)
{
dvz_allocator_destroy(app->allocator);
dvz_allocator_free(app->allocator);
app->allocator = NULL;
}
dvz_device_destroy(app->device);
app->device = NULL;
}
if (app->instance != NULL)
{
dvz_instance_destroy(app->instance);
app->instance = NULL;
}
}
/**
* Run the interactive render loop.
*
* @param app initialized app state
* @returns process exit code (0 on success)
*/
static int _dvz_canvas_run(DvzCanvasApp* app)
{
ANN(app);
ANN(app->host);
ANN(app->canvas);
time_t start_time = time(NULL);
uint64_t submitted_frames = 0;
if (app->options.start_recording)
{
_dvz_canvas_toggle_recording(app);
}
if (!_dvz_canvas_benchmark_begin(app))
{
return 1;
}
while (app->running)
{
dvz_window_host_poll(app->host);
_dvz_canvas_poll_keyboard_shortcuts(app);
if (_dvz_canvas_window_should_close(app))
{
break;
}
if (app->toggle_record_requested)
{
app->toggle_record_requested = false;
_dvz_canvas_toggle_recording(app);
}
if (app->screenshot_requested)
{
app->screenshot_requested = false;
char path[1024] = {0};
_dvz_canvas_screenshot_path(app, path, sizeof(path));
if (dvz_canvas_capture_png(app->canvas, path) == 0)
{
dvz_fprintf(stderr, "screenshot: %s\n", path);
}
else
{
dvz_fprintf(stderr, "failed to capture screenshot\n");
}
}
int frame_rc = dvz_canvas_frame(app->canvas);
if (frame_rc == DVZ_CANVAS_FRAME_WAIT_SURFACE)
{
continue;
}
if (frame_rc != DVZ_CANVAS_FRAME_READY)
{
dvz_fprintf(stderr, "canvas frame error: %d\\n", frame_rc);
break;
}
if (dvz_canvas_submit(app->canvas) != 0)
{
dvz_fprintf(stderr, "canvas submit error\\n");
break;
}
_dvz_canvas_report_present_mode(app);
submitted_frames++;
_dvz_canvas_benchmark_record(app, submitted_frames);
if (app->options.max_frames > 0 && submitted_frames >= app->options.max_frames)
{
break;
}
if (app->options.duration_s > 0.0)
{
time_t now = time(NULL);
double elapsed = difftime(now, start_time);
if (elapsed >= app->options.duration_s)
{
break;
}
}
}
if (app->device != NULL)
{
dvz_device_wait(app->device);
}
_dvz_canvas_benchmark_end(app);
return 0;
}
/*************************************************************************************************/
/* Entry point */
/*************************************************************************************************/
/**
* Entry point for the interactive canvas smoke executable.
*
* @param argc argument count
* @param argv argument vector
* @returns process exit code
*/
int main(int argc, char** argv)
{
DvzCanvasApp app = {0};
log_set_level_env();
_dvz_canvas_options_default(&app.options);
for (int i = 1; i < argc; ++i)
{
const char* arg = argv[i];
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0)
{
_dvz_canvas_usage();
return 0;
}
}
if (!_dvz_canvas_parse_args(argc, argv, &app.options))
{
return 1;
}
if (!_dvz_canvas_init(&app))
{
_dvz_canvas_destroy(&app);
return 1;
}
int rc = _dvz_canvas_run(&app);
_dvz_canvas_destroy(&app);
return rc;
}