1546 lines
43 KiB
C
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;
|
|
}
|