ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
renderer.c
Go to the documentation of this file.
10#include <ascii-chat/os/font.h>
12#include <ascii-chat/log/log.h>
13#include <libplatform/terminal.h>
16#include <string.h>
21#include <ascii-chat/font.h>
22
23#define RENDER_AUTO_FONT_SIZE_PT 12.0
24#define RENDER_AUTO_VIDEO_MAX_DIMENSION 1280
25#define RENDER_AUTO_MIN_FONT_SIZE_PT 3.0
26
52
53asciichat_error_t render_file_create(const char *output_path, int cols, int rows, int fps, int theme,
55 if (!output_path || !out || cols <= 0 || rows <= 0)
56 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing render output, grid, or context");
57 *out = NULL;
58 log_info("output_path=%s, cols=%d, rows=%d, fps=%d, theme=%d, out=%p", output_path ? output_path : "(null)", cols,
59 rows, fps, theme, (void *)out);
60 render_file_ctx_t *ctx = SAFE_CALLOC(1, sizeof(*ctx), render_file_ctx_t *);
61 if (strcmp(output_path, "/dev/null") == 0) {
62 ctx->discard = true;
63 ctx->error = ASCIICHAT_OK;
64 *out = ctx;
65 return ASCIICHAT_OK;
66 }
67 ctx->kitty_only = strcmp(output_path, "--kitty") == 0;
68 ctx->kitty = ctx->kitty_only || GET_OPTION(render_kitty);
69 if (ctx->kitty && !GET_OPTION(snapshot_mode)) {
70 SAFE_FREE(ctx);
71 return SET_ERRNO(ERROR_INVALID_PARAM, "Kitty graphics output requires --snapshot");
72 }
73 if (ctx->kitty && (strcmp(output_path, "-") == 0 || strcmp(output_path, "pipe:") == 0)) {
74 SAFE_FREE(ctx);
75 return SET_ERRNO(ERROR_INVALID_PARAM, "Kitty graphics and binary media cannot share stdout");
76 }
77 const char *term = SAFE_GETENV("TERM");
78 if (ctx->kitty && terminal_is_stdout_tty() &&
79 (!term || strcmp(term, "dumb") == 0 || strcmp(term, "linux") == 0 || strcmp(term, "vt100") == 0)) {
80 SAFE_FREE(ctx);
82 "Kitty graphics require a capable terminal; redirect stdout to export the protocol");
83 }
84
85 // Resolve font spec → platform-appropriate file path, family name, or bundled data.
86 char font_spec[PLATFORM_MAX_PATH_LENGTH] = {0};
87 bool font_is_path = false;
88 const uint8_t *font_data = NULL;
89 size_t font_data_size = 0;
90 const char *raw_font = GET_OPTION(render_font);
91
92 // Log the font option value from GET_OPTION (for WASM font debugging)
93 log_info("[WASM_FONT_GETTER] GET_OPTION(render_font) returned: '%s' (empty=%d), GET_OPTION(matrix_rain)=%d",
94 raw_font ? raw_font : "(null)", (!raw_font || raw_font[0] == '\0') ? 1 : 0, GET_OPTION(matrix_rain));
95
96 // Auto-select matrix font when --matrix flag is set, unless user explicitly overrides with --render-font
97 log_debug("raw_font=%s, matrix_rain=%d", raw_font, GET_OPTION(matrix_rain));
98 if ((!raw_font || raw_font[0] == '\0') && GET_OPTION(matrix_rain)) {
99 raw_font = "matrix";
100 log_debug("[MATRIX] Using matrix font (enabled by --matrix flag)");
101 }
102
103 platform_font_register("matrix", g_font_matrix_resurrected, g_font_matrix_resurrected_size);
104 platform_font_register("default", g_font_default, g_font_default_size);
105 platform_font_register("DejaVu Sans Mono", g_font_default, g_font_default_size);
107 platform_font_resolve(raw_font, font_spec, sizeof(font_spec), &font_is_path, &font_data, &font_data_size);
108 if (fe != ASCIICHAT_OK) {
109 if (raw_font && raw_font[0]) {
110 SAFE_FREE(ctx);
111 return fe;
112 }
113 log_warn("font resolution failed for '%s' — using bundled default", raw_font ? raw_font : "(explicit system)");
114 fe = platform_font_resolve("default", font_spec, sizeof(font_spec), &font_is_path, &font_data, &font_data_size);
115 if (fe != ASCIICHAT_OK) {
116 SAFE_FREE(ctx);
117 return fe;
118 }
119 }
120
121 log_debug("Font resolved: font_spec='%s', font_is_path=%d, font_data=%p (size=%zu)", font_spec, font_is_path,
122 (void *)font_data, font_data_size);
123
124 // A zero size selects a compact default for video; explicit font sizes keep
125 // their exact dimensions. Images and Kitty snapshots retain the 12 pt default.
126 double font_size_pt = GET_OPTION(render_font_size);
127 const char *extension = strrchr(output_path, '.');
128 bool is_image =
129 extension && (!strcmp(extension, ".png") || !strcmp(extension, ".jpg") || !strcmp(extension, ".jpeg") ||
130 !strcmp(extension, ".bmp") || !strcmp(extension, ".webp"));
131 bool automatic_video_size = font_size_pt == 0.0 && !is_image && !ctx->kitty;
132 if (font_size_pt == 0.0) {
133 font_size_pt = RENDER_AUTO_FONT_SIZE_PT;
134 if (automatic_video_size) {
135 double width_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / ((double)cols * 10.0);
136 double height_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / ((double)rows * 20.0);
137 double scale = width_scale < height_scale ? width_scale : height_scale;
138 if (scale < 1.0)
139 font_size_pt *= scale;
140 if (font_size_pt < RENDER_AUTO_MIN_FONT_SIZE_PT)
141 font_size_pt = RENDER_AUTO_MIN_FONT_SIZE_PT;
142 }
143 }
144 term_renderer_theme_t resolved_theme = (term_renderer_theme_t)theme;
145 if (resolved_theme == TERM_RENDERER_THEME_AUTO) {
146 resolved_theme =
148 log_debug("auto theme resolved to %s", resolved_theme == TERM_RENDERER_THEME_DARK ? "dark" : "light");
149 }
150
151 term_renderer_config_t tr_cfg = {
152 .cols = cols,
153 .rows = rows,
154 .font_size_pt = font_size_pt,
155 .theme = resolved_theme,
156 .font_is_path = font_is_path,
157 .font_data = font_data,
158 .font_data_size = font_data_size,
159 };
160 SAFE_STRNCPY(tr_cfg.font_spec, font_spec, sizeof(tr_cfg.font_spec));
161
162 asciichat_error_t err = term_renderer_create(&tr_cfg, &ctx->renderer);
163 if (err != ASCIICHAT_OK) {
164 log_error("term_renderer_create failed: %s", asciichat_error_format(NULL, err));
165 SAFE_FREE(ctx);
166 return err;
167 }
168 int actual_width_px = term_renderer_width_px(ctx->renderer);
169 int actual_height_px = term_renderer_height_px(ctx->renderer);
170 // Hinting and font metrics vary. Recalculate once if the initial estimate
171 // exceeds the target, retaining the original renderer if a bitmap font
172 // cannot be scaled down.
173 if (automatic_video_size &&
174 (actual_width_px > RENDER_AUTO_VIDEO_MAX_DIMENSION || actual_height_px > RENDER_AUTO_VIDEO_MAX_DIMENSION) &&
175 font_size_pt > RENDER_AUTO_MIN_FONT_SIZE_PT) {
176 double width_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / actual_width_px;
177 double height_scale = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / actual_height_px;
178 double scale = width_scale < height_scale ? width_scale : height_scale;
179 double smaller = font_size_pt * scale * 0.98;
180 if (smaller < RENDER_AUTO_MIN_FONT_SIZE_PT)
182 tr_cfg.font_size_pt = smaller;
183 terminal_renderer_t *candidate = NULL;
184 err = term_renderer_create(&tr_cfg, &candidate);
185 if (err == ASCIICHAT_OK) {
186 int candidate_width = term_renderer_width_px(candidate);
187 int candidate_height = term_renderer_height_px(candidate);
188 if (candidate_width <= actual_width_px && candidate_height <= actual_height_px &&
189 (candidate_width < actual_width_px || candidate_height < actual_height_px)) {
191 ctx->renderer = candidate;
192 actual_width_px = candidate_width;
193 actual_height_px = candidate_height;
194 } else {
195 term_renderer_destroy(candidate);
196 }
197 } else {
199 SAFE_FREE(ctx);
200 return err;
201 }
202 }
203 log_info("GRID DIMENSIONS: %ux%u cells -> PIXEL DIMENSIONS: %dx%d px", cols, rows, actual_width_px, actual_height_px);
204 log_debug("term_renderer created (%dx%d cells, %dx%d px)", cols, rows, actual_width_px, actual_height_px);
205
206 if (ctx->kitty_only) {
207 ctx->fps = fps;
208 ctx->progress = render_progress_create(true, "terminal");
209 NAMED_REGISTER_CONTEXT(ctx, "render_file_ctx", output_path, NULL);
211 *out = ctx;
212 return ASCIICHAT_OK;
213 }
214 int output_width_px = actual_width_px;
215 int output_height_px = actual_height_px;
216 if (automatic_video_size &&
217 (output_width_px > RENDER_AUTO_VIDEO_MAX_DIMENSION || output_height_px > RENDER_AUTO_VIDEO_MAX_DIMENSION)) {
218 double scale_x = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / output_width_px;
219 double scale_y = (double)RENDER_AUTO_VIDEO_MAX_DIMENSION / output_height_px;
220 double scale = scale_x < scale_y ? scale_x : scale_y;
221 output_width_px = ((int)(output_width_px * scale)) & ~1;
222 output_height_px = ((int)(output_height_px * scale)) & ~1;
223 if (output_width_px < 2)
224 output_width_px = 2;
225 if (output_height_px < 2)
226 output_height_px = 2;
227 }
228 err = ffmpeg_encoder_create_scaled(output_path, actual_width_px, actual_height_px, output_width_px, output_height_px,
229 fps, &ctx->encoder);
230 if (err != ASCIICHAT_OK) {
231 log_error("ffmpeg_encoder_create failed: %s", asciichat_error_format(NULL, err));
233 SAFE_FREE(ctx);
234 return err;
235 }
236 log_debug("ffmpeg_encoder created successfully");
237
238 // Initialize audio fields
239 ctx->audio_sample_rate = 48000; // Audio pipeline is 48kHz
240 ctx->fps = fps;
241 // Allocate large buffer for snapshot mode (5 seconds at 48kHz with ~14 FPS = 3,428 samples per frame)
242 // Use 8192 to comfortably handle snapshot reads without excessive overhead
243 ctx->audio_buf_size = 8192; // Temporary buffer for reading audio (in floats)
244 ctx->audio_read_buf = SAFE_MALLOC(ctx->audio_buf_size * sizeof(float), float *);
245
247 if (err != ASCIICHAT_OK) {
251 SAFE_FREE(ctx);
252 return err;
253 }
254 log_info("initialized encoder for %s", output_path);
255
256 // Register render file context for debugging
257 NAMED_REGISTER_CONTEXT(ctx, "render_file_ctx", output_path, NULL);
258
259 bool binary_stdout = strcmp(output_path, "-") == 0 || strcmp(output_path, "pipe:") == 0;
260 ctx->progress = render_progress_create(!binary_stdout, output_path);
262 *out = ctx;
263 return ASCIICHAT_OK;
264}
265
267 if (ctx) {
268 ctx->live_timing = true;
269 if (ctx->encoder)
271 }
272}
273
274static asciichat_error_t render_file_write_frame_internal(render_file_ctx_t *ctx, const char *ansi_frame,
275 uint64_t captured_ns) {
276
277 if (!ctx) {
278 log_warn("ctx is NULL");
279 return ASCIICHAT_OK;
280 }
281
282 if (!ansi_frame) {
283 log_warn("ansi_frame is NULL");
284 return ASCIICHAT_OK;
285 }
286
287 size_t frame_len = strlen(ansi_frame);
288 uint64_t frame_index = ctx->frames_written;
289 if (ctx->live_timing && ctx->frames_written > 0) {
290 uint64_t elapsed = captured_ns > ctx->first_frame_ns ? captured_ns - ctx->first_frame_ns : 0;
291 frame_index = elapsed / 1000000000ULL * ctx->fps + elapsed % 1000000000ULL * ctx->fps / 1000000000ULL;
292 if (frame_index <= ctx->last_frame_index)
293 return ASCIICHAT_OK;
294 }
295 uint64_t render_started_ns = time_get_ns();
296 asciichat_error_t err = term_renderer_feed(ctx->renderer, ansi_frame, frame_len);
297 uint64_t rasterized_ns = time_get_ns();
298 if (err != ASCIICHAT_OK) {
299 log_warn("term_renderer_feed failed: %s", asciichat_error_format(NULL, err));
300 return err;
301 }
302 if (ctx->kitty_only) {
303 ctx->frames_written++;
304 return ASCIICHAT_OK;
305 }
306
307 const uint8_t *pixels = term_renderer_pixels(ctx->renderer);
308 int pitch = term_renderer_pitch(ctx->renderer);
309 // sws_scale consumes the renderer's pixels synchronously before returning.
310 // The renderer is owned by this encoder thread and is not updated until the
311 // next call, so copying the entire RGBA frame is unnecessary.
312 err = ffmpeg_encoder_write_frame(ctx->encoder, pixels, pitch, captured_ns);
313 uint64_t encoded_ns = time_get_ns();
314 log_debug_every(US_PER_SEC_INT, "RECORD_STAGE: raster_ms=%.2f encode_ms=%.2f bytes=%zu",
315 (double)(rasterized_ns - render_started_ns) / NS_PER_MS_INT,
316 (double)(encoded_ns - rasterized_ns) / NS_PER_MS_INT, frame_len);
317 if (err != ASCIICHAT_OK) {
318 log_warn("ffmpeg_encoder_write_frame failed: %s", asciichat_error_format(NULL, err));
319 }
320
321 if (err == ASCIICHAT_OK) {
322 if (ctx->frames_written == 0) {
323 ctx->first_frame_ns = captured_ns;
324 audio_recording_start(ctx->recording, captured_ns);
325 }
326 ctx->frames_written++;
327 ctx->last_frame_index = frame_index;
328 uint64_t target =
329 ((ctx->live_timing ? frame_index + 1 : ctx->frames_written) * ctx->audio_sample_rate / ctx->fps) * 2;
330 ctx->audio_target = target;
331 /* Keep 100 ms pending so audio callbacks can deliver samples before encoding. */
332 uint64_t ready = target > 9600 ? target - 9600 : 0;
333 while (ctx->audio_samples_written < ready) {
334 int count = (int)((ready - ctx->audio_samples_written) > (uint64_t)ctx->audio_buf_size
336 : ready - ctx->audio_samples_written);
338 err = ffmpeg_encoder_write_audio(ctx->encoder, ctx->audio_read_buf, count);
339 if (err != ASCIICHAT_OK)
340 break;
341 ctx->audio_samples_written += count;
342 }
343 }
344
345 return err;
346}
347
349 if (!ctx || !frame)
350 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing render context or frame");
351 if (ctx->error != ASCIICHAT_OK)
352 return ctx->error;
353 if (ctx->discard)
354 return ASCIICHAT_OK;
355 asciichat_error_t err = render_file_write_frame_internal(ctx, frame, captured_ns);
356 if (err != ASCIICHAT_OK)
358 render_progress_frame(ctx->progress, frame, err == ASCIICHAT_OK);
359 if (err != ASCIICHAT_OK)
360 ctx->error = err;
361 return err;
362}
363
364void render_file_begin_drain(render_file_ctx_t *ctx, uint64_t total, bool total_known) {
365 if (ctx)
366 render_progress_begin(ctx->progress, total, total_known);
367}
368
369void render_file_set_snapshot_actual_duration(render_file_ctx_t *ctx, double actual_duration_sec) {
370 if (!ctx || !ctx->encoder)
371 return;
372 ffmpeg_encoder_set_snapshot_actual_duration(ctx->encoder, actual_duration_sec);
373}
374
376 if (!ctx)
377 return ASCIICHAT_OK;
378 if (ctx->discard) {
379 SAFE_FREE(ctx);
380 return ASCIICHAT_OK;
381 }
382 NAMED_UNREGISTER(ctx);
385 uint64_t target = ctx->audio_target;
387 while (ctx->audio_samples_written < target) {
388 int count = (int)((target - ctx->audio_samples_written) > (uint64_t)ctx->audio_buf_size
390 : target - ctx->audio_samples_written);
392 err = ffmpeg_encoder_write_audio(ctx->encoder, ctx->audio_read_buf, count);
393 if (err != ASCIICHAT_OK)
394 break;
395 ctx->audio_samples_written += count;
396 }
399 if (err == ASCIICHAT_OK)
400 err = close_err;
401 if (ctx->error != ASCIICHAT_OK)
402 err = ctx->error;
404 if (err == ASCIICHAT_OK && ctx->kitty && ctx->frames_written == 0)
405 err = SET_ERRNO(ERROR_MEDIA_INIT, "No snapshot was rendered for Kitty output");
406 if (err == ASCIICHAT_OK && ctx->kitty && ctx->frames_written > 0)
412 SAFE_FREE(ctx);
413 return err;
414}
const char * asciichat_error_format(const char *message, asciichat_error_t code)
#define SET_ERRNO(code,...)
terminal_background_t colorscheme_current_background(void)
@ TERM_BACKGROUND_DARK
Definition colorscheme.h:67
Named object registry for debugging — log identifiable resource names.
void ffmpeg_encoder_set_snapshot_actual_duration(ffmpeg_encoder_t *enc, double actual_duration_sec)
asciichat_error_t ffmpeg_encoder_create_scaled(const char *output_path, int source_width, int source_height, int width_px, int height_px, int fps, ffmpeg_encoder_t **out)
void ffmpeg_encoder_set_live_timing(ffmpeg_encoder_t *enc)
asciichat_error_t ffmpeg_encoder_write_frame(ffmpeg_encoder_t *enc, const uint8_t *rgb, int pitch, uint64_t captured_ns)
asciichat_error_t ffmpeg_encoder_destroy(ffmpeg_encoder_t *enc)
asciichat_error_t ffmpeg_encoder_write_audio(ffmpeg_encoder_t *enc, const float *samples, int num_samples)
FFmpeg video/image file encoder — codec selected from file extension.
struct ffmpeg_encoder_s ffmpeg_encoder_t
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_GETENV(name)
Definition common.h:449
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define NAMED_REGISTER_CONTEXT(context, context_type, name, parent_ptr)
Register a generic context with automatic format specifier.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_INVALID_PARAM
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:69
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_MS_INT
Definition time.h:168
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
Font resolution and bundled font data.
const size_t g_font_matrix_resurrected_size
const unsigned char g_font_matrix_resurrected[]
const size_t g_font_default_size
const unsigned char g_font_default[]
📝 Logging API with multiple log levels and terminal output control
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
asciichat_error_t kitty_image_write(FILE *stream, const uint8_t *rgba, int width, int height, int pitch)
int term_renderer_width_px(terminal_renderer_t *r)
const uint8_t * term_renderer_pixels(terminal_renderer_t *r)
asciichat_error_t term_renderer_create(const term_renderer_config_t *cfg, terminal_renderer_t **out)
void term_renderer_destroy(terminal_renderer_t *r)
asciichat_error_t term_renderer_feed(terminal_renderer_t *r, const char *ansi_frame, size_t len)
int term_renderer_pitch(terminal_renderer_t *r)
int term_renderer_height_px(terminal_renderer_t *r)
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
static bool size_t gotel_query_t * out
Definition queries.h:8
asciichat_error_t audio_recording_create_channels(audio_recording_t **out, unsigned channels)
Definition recording.c:34
void audio_recording_read(audio_recording_t *recording, float *samples, size_t count)
Definition recording.c:127
void audio_recording_destroy(audio_recording_t *recording)
Definition recording.c:54
void audio_recording_start(audio_recording_t *recording, uint64_t epoch_ns)
Definition recording.c:67
struct audio_recording audio_recording_t
Definition recording.h:8
void render_progress_finalize(render_progress_t *progress)
render_progress_t * render_progress_create(bool enabled, const char *destination)
void render_progress_frame(render_progress_t *progress, const char *frame, bool succeeded)
void render_progress_begin(render_progress_t *progress, uint64_t total, bool total_known)
void render_progress_destroy(render_progress_t *progress)
struct render_progress render_progress_t
void render_file_set_snapshot_actual_duration(render_file_ctx_t *ctx, double actual_duration_sec)
Definition renderer.c:369
asciichat_error_t render_file_write_frame(render_file_ctx_t *ctx, const char *frame, uint64_t captured_ns)
Definition renderer.c:348
void render_file_set_live_timing(render_file_ctx_t *ctx)
Definition renderer.c:266
asciichat_error_t render_file_create(const char *output_path, int cols, int rows, int fps, int theme, render_file_ctx_t **out)
Definition renderer.c:53
#define RENDER_AUTO_VIDEO_MAX_DIMENSION
Definition renderer.c:24
#define RENDER_AUTO_FONT_SIZE_PT
Definition renderer.c:23
#define RENDER_AUTO_MIN_FONT_SIZE_PT
Definition renderer.c:25
asciichat_error_t render_file_destroy(render_file_ctx_t *ctx)
Definition renderer.c:375
void render_file_begin_drain(render_file_ctx_t *ctx, uint64_t total, bool total_known)
Definition renderer.c:364
Terminal-to-pixel renderer and render-file output (audio + video).
struct terminal_renderer_s terminal_renderer_t
Definition renderer.h:18
term_renderer_theme_t
Terminal rendering theme.
Definition renderer.h:27
@ TERM_RENDERER_THEME_LIGHT
Definition renderer.h:29
@ TERM_RENDERER_THEME_DARK
Definition renderer.h:28
@ TERM_RENDERER_THEME_AUTO
Definition renderer.h:30
struct render_file_ctx_s render_file_ctx_t
Definition renderer.h:63
🔤 String Manipulation and Shell Escaping Utilities
For render file ctx, this is the live record that ties progress, error, renderer, encoder together....
Definition renderer.c:32
uint64_t audio_samples_written
Definition renderer.c:39
asciichat_error_t error
Definition renderer.c:34
render_progress_t * progress
Definition renderer.c:33
terminal_renderer_t * renderer
Definition renderer.c:35
uint64_t frames_written
Definition renderer.c:38
uint64_t first_frame_ns
Definition renderer.c:40
ffmpeg_encoder_t * encoder
Definition renderer.c:36
uint32_t audio_sample_rate
Definition renderer.c:47
uint64_t audio_target
Definition renderer.c:42
audio_recording_t * recording
Definition renderer.c:37
float * audio_read_buf
Definition renderer.c:49
uint64_t last_frame_index
Definition renderer.c:41
char font_spec[PLATFORM_MAX_PATH_LENGTH]
Definition renderer.h:38
⏱️ High-precision timing utilities using sokol_time.h and uthash
void ui_controller_title_render(ui_title_render_t phase)
@ UI_TITLE_FINALIZING
@ UI_TITLE_RENDERING
@ UI_TITLE_FAILED
@ UI_TITLE_IDLE