ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
http_renderer.c
Go to the documentation of this file.
1#include "http_renderer.h"
2
10#include <libplatform/system.h>
12#include <string.h>
13
21
22static void *http_renderer_service_main(void *argument);
23
24static bool http_renderer_cancel_requested(void *context) {
25 return context && atomic_load_bool((atomic_t *)context);
26}
27
28static void *http_renderer_job_main(void *argument) {
29 http_renderer_job_t *job = argument;
30 if (job->renderer)
31 (void)http_renderer_service_main(job);
32 else
33 job->execute(job->context, &job->cancelled);
34 atomic_store_bool(&job->done, true);
35 if (job->renderer) {
36 if (job->result.error == ASCIICHAT_OK) {
37 if (job->renderer->config.on_complete)
38 job->renderer->config.on_complete(job, &job->result, job->renderer->config.user_data);
39 } else if (job->renderer->config.on_error) {
41 }
42 }
43 return NULL;
44}
45
47 void *context) {
48 if (!job || !execute || job->started)
49 return SET_ERRNO(ERROR_INVALID_STATE, "Renderer job is already active");
50 memset(job, 0, sizeof(*job));
51 job->execute = execute;
52 job->context = context;
53 atomic_store_bool(&job->cancelled, false);
54 atomic_store_bool(&job->done, false);
55 if (platform_thread_create(&job->thread, name ? name : "http-render", http_renderer_job_main, job))
56 return SET_ERRNO(ERROR_THREAD, "Cannot start renderer job");
57 job->started = true;
58 return ASCIICHAT_OK;
59}
60
62 if (job && job->started)
63 atomic_store_bool(&job->cancelled, true);
64}
65
67 return job && job->started && atomic_load_bool((atomic_t *)&job->done);
68}
69
71 if (job && job->started) {
72 thread_join(&job->thread, NULL);
73 job->started = false;
74 }
75}
76
78 if (job) {
80 memset(job, 0, sizeof(*job));
81 }
82}
83
84static void *http_renderer_service_main(void *argument) {
85 http_renderer_job_t *job = argument;
86 http_renderer_t *renderer = job->renderer;
88 memset(&job->result, 0, sizeof(job->result));
89 job->result.mime_type[0] = '\0';
90 if (atomic_load_bool(&job->cancelled)) {
91 job->result.cancelled = true;
92 error = SET_ERRNO(ERROR_INVALID_STATE, "HTTP render cancelled");
93 } else if (job->request.source.type != HTTP_RENDER_SOURCE_UPLOAD &&
96 error = SET_ERRNO(ERROR_INVALID_PARAM, "HTTP renderer source is not supported");
97 } else if (renderer->config.max_input_bytes && job->request.source.body_length > renderer->config.max_input_bytes) {
98 error = SET_ERRNO(ERROR_BUFFER_OVERFLOW, "HTTP render input exceeds the configured limit");
99 } else {
100 const char *source =
102 error = http_renderer_render_file(source, &job->request, &job->result, &job->cancelled);
103 }
104 job->result.error = error;
105 return NULL;
106}
107
109 if (!out || !config)
110 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing HTTP renderer configuration");
111 *out = NULL;
112 http_renderer_t *renderer = SAFE_CALLOC(1, sizeof(*renderer), http_renderer_t *);
113 renderer->config = *config;
114 if (!renderer->config.max_jobs)
115 renderer->config.max_jobs = 1;
116 if (!renderer->config.max_output_bytes)
118 if (mutex_init(&renderer->lock, "http_renderer") != 0) {
119 SAFE_FREE(renderer);
120 return SET_ERRNO(ERROR_THREAD, "Cannot initialize HTTP renderer lock");
121 }
122 *out = renderer;
123 return ASCIICHAT_OK;
124}
125
127 http_renderer_job_t **out_job) {
128 if (!renderer || !request || !out_job)
129 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid HTTP renderer submission");
130 *out_job = NULL;
131 mutex_lock(&renderer->lock);
132 if (renderer->stopping || renderer->active_jobs >= renderer->config.max_jobs) {
133 mutex_unlock(&renderer->lock);
134 return SET_ERRNO(ERROR_BUFFER_FULL, "HTTP renderer has no available job capacity");
135 }
136 http_renderer_job_t *job = SAFE_CALLOC(1, sizeof(*job), http_renderer_job_t *);
137 job->renderer = renderer;
138 job->request = *request;
139 job->user_data = user_data;
140 if (!job->request.timeout_ms)
141 job->request.timeout_ms = renderer->config.timeout_ms;
142 if (!job->request.max_output_bytes)
144 atomic_store_bool(&job->cancelled, false);
145 atomic_store_bool(&job->done, false);
146 if (platform_thread_create(&job->thread, "http-render", http_renderer_job_main, job)) {
147 SAFE_FREE(job);
148 mutex_unlock(&renderer->lock);
149 return SET_ERRNO(ERROR_THREAD, "Cannot start HTTP renderer job");
150 }
151 job->started = true;
152 job->next = renderer->jobs;
153 renderer->jobs = job;
154 ++renderer->active_jobs;
155 mutex_unlock(&renderer->lock);
156 *out_job = job;
157 return ASCIICHAT_OK;
158}
159
161 if (!job || !job->renderer)
162 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid HTTP renderer job");
164 return ASCIICHAT_OK;
165}
166
170
171void http_renderer_reap(http_renderer_t *renderer, bool all) {
172 if (!renderer)
173 return;
174 for (;;) {
175 mutex_lock(&renderer->lock);
176 http_renderer_job_t **link = &renderer->jobs;
177 http_renderer_job_t *job = NULL;
178 while (*link) {
179 if (all || ((*link)->user_data == NULL && http_renderer_job_is_done(*link))) {
180 job = *link;
181 *link = job->next;
182 --renderer->active_jobs;
183 break;
184 }
185 link = &(*link)->next;
186 }
187 mutex_unlock(&renderer->lock);
188 if (!job)
189 return;
192 SAFE_FREE(job);
193 }
194}
195
197 if (!renderer)
198 return;
199 mutex_lock(&renderer->lock);
200 renderer->stopping = true;
201 for (http_renderer_job_t *job = renderer->jobs; job; job = job->next)
203 mutex_unlock(&renderer->lock);
204}
205
207 if (!renderer || !*renderer)
208 return;
209 http_renderer_stop(*renderer);
210 http_renderer_reap(*renderer, true);
211 mutex_destroy(&(*renderer)->lock);
212 SAFE_FREE(*renderer);
213}
214#include <stdlib.h>
215
217 if (!request)
218 return;
219 memset(request, 0, sizeof(*request));
222}
223
225 if (!result)
226 return;
227 SAFE_FREE(result->ascii);
228 for (size_t i = 0; i < result->frame_count; ++i)
229 SAFE_FREE(result->frames[i].text);
230 SAFE_FREE(result->frames);
231 result->frame_count = 0;
232 result->frame_capacity = 0;
233 result->ascii_length = 0;
234 result->output_path[0] = '\0';
235 result->mime_type[0] = '\0';
236}
237
238static asciichat_error_t http_render_result_reserve_frame(http_render_result_t *result) {
240 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Rendered frame count exceeds the configured limit");
241 if (result->frame_count == result->frame_capacity) {
242 size_t capacity = result->frame_capacity ? result->frame_capacity * 2 : 4;
243 if (capacity > HTTP_RENDERER_MAX_FRAMES)
244 capacity = HTTP_RENDERER_MAX_FRAMES;
245 result->frames = realloc(result->frames, capacity * sizeof(*result->frames));
246 if (!result->frames)
247 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate rendered frame list");
248 memset(result->frames + result->frame_capacity, 0, (capacity - result->frame_capacity) * sizeof(*result->frames));
249 result->frame_capacity = capacity;
250 }
251 return ASCIICHAT_OK;
252}
253
255 uint64_t timestamp_ms) {
256 if (!result || (!text && length))
257 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid text frame");
258 asciichat_error_t err = http_render_result_reserve_frame(result);
259 if (err != ASCIICHAT_OK)
260 return err;
261 http_render_frame_t *frame = &result->frames[result->frame_count++];
262 frame->text = SAFE_MALLOC(length + 1, char *);
263 SAFE_MEMCPY(frame->text, length + 1, text, length);
264 frame->text[length] = '\0';
265 frame->text_length = length;
266 frame->timestamp_ms = timestamp_ms;
267 return ASCIICHAT_OK;
268}
269
271 uint64_t timestamp_ms) {
272 if (!result || !path || !*path)
273 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid file frame");
274 asciichat_error_t err = http_render_result_reserve_frame(result);
275 if (err != ASCIICHAT_OK)
276 return err;
277 http_render_frame_t *frame = &result->frames[result->frame_count++];
278 SAFE_STRNCPY(frame->path, path, sizeof(frame->path));
279 frame->timestamp_ms = timestamp_ms;
280 return ASCIICHAT_OK;
281}
282
283static char *http_renderer_render_image(const image_t *image, const http_render_request_t *request) {
284 size_t width = request->options.width > 0 ? (size_t)request->options.width : 80;
285 size_t height = request->options.height > 0 ? (size_t)request->options.height
286 : (size_t)((double)width * image->h / image->w / 2.0);
287 if (!height)
288 height = 1;
289 image_t *scaled = image_new(width, height);
290 if (!scaled)
291 return NULL;
292 image_resize(image, scaled);
293 const char *palette = request->options.palette_chars[0] ? request->options.palette_chars : NULL;
294 if (!palette && request->options.palette[0]) {
295 const struct {
296 const char *name;
297 palette_type_t type;
298 } palettes[] = {{"standard", PALETTE_STANDARD},
299 {"blocks", PALETTE_BLOCKS},
300 {"digital", PALETTE_DIGITAL},
301 {"minimal", PALETTE_MINIMAL},
302 {"cool", PALETTE_COOL}};
303 for (size_t i = 0; i < sizeof(palettes) / sizeof(palettes[0]); ++i)
304 if (!strcmp(request->options.palette, palettes[i].name)) {
305 palette = get_builtin_palette(palettes[i].type)->chars;
306 break;
307 }
308 if (!palette)
309 palette = request->options.palette;
310 }
311 if (!palette)
312 palette = DEFAULT_ASCII_PALETTE;
313 char *ascii;
314 if (!strcmp(request->options.render_theme, "braille"))
315 ascii = NULL;
316 else if (!strcmp(request->options.render_theme, "block") || !strcmp(request->options.render_theme, "half-block")) {
317 ascii_color_t color = request->options.color_mode == TERM_COLOR_TRUECOLOR ? ASCII_COLOR_TRUE
318 : request->options.color_mode == TERM_COLOR_256 ? ASCII_COLOR_256
319 : request->options.color_mode == TERM_COLOR_16 ? ASCII_COLOR_16
321 ascii =
322 ascii_render_halfblocks((const uint8_t *)scaled->pixels, scaled->w, scaled->h, 0, color, simd_cells_backend());
323 } else if (request->options.color_mode == TERM_COLOR_TRUECOLOR)
324 ascii = image_print_color_simd(scaled, false, false, palette);
325 else if (request->options.color_mode == TERM_COLOR_256)
326 ascii = ascii_render(scaled, palette, ASCII_COLOR_256, false, false, simd_cells_backend());
327 else if (request->options.color_mode == TERM_COLOR_16)
328 ascii = ascii_render(scaled, palette, ASCII_COLOR_16, false, false, simd_cells_backend());
329 else
330 ascii = image_print(scaled, palette);
331 image_destroy(scaled);
332 return ascii;
333}
334
336 http_render_result_t *result, atomic_t *cancelled) {
337 if (!path || !request || !result)
338 return SET_ERRNO(ERROR_INVALID_PARAM, "HTTP render requires a source path, request, and result");
339 memset(result, 0, sizeof(*result));
340 uint64_t started = time_get_ns();
341 ffmpeg_decoder_t *decoder = ffmpeg_decoder_create_interruptible(path, http_renderer_cancel_requested, cancelled);
342 if (!decoder)
343 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot decode uploaded image");
345 ffmpeg_decoder_destroy(decoder);
346 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot start image decoder");
347 }
348 image_t *image = NULL;
349 uint64_t deadline = started + (request->timeout_ms ? request->timeout_ms : 10000) * NS_PER_MS_INT;
350 for (;;) {
351 if (cancelled && atomic_load_bool(cancelled)) {
352 ffmpeg_decoder_destroy(decoder);
353 result->cancelled = true;
354 return SET_ERRNO(ERROR_INVALID_STATE, "HTTP image render cancelled");
355 }
356 asciichat_error_context_t failure = {0};
357 ffmpeg_frame_status_t status = ffmpeg_decoder_poll_video(decoder, &image, &failure);
358 if (status == FFMPEG_FRAME_READY)
359 break;
360 if (status == FFMPEG_FRAME_FAILED || status == FFMPEG_FRAME_EOF) {
361 errno_import(&failure);
362 ffmpeg_decoder_destroy(decoder);
363 return SET_ERRNO(ERROR_MEDIA_INIT, "Uploaded data does not contain a decodable image");
364 }
365 if (time_get_ns() >= deadline) {
366 ffmpeg_decoder_destroy(decoder);
367 return SET_ERRNO(ERROR_MEDIA_DECODE, "Image decoding timed out");
368 }
370 }
371 if (!image) {
372 ffmpeg_decoder_destroy(decoder);
373 return SET_ERRNO(ERROR_MEDIA_INIT, "Uploaded data does not contain a decodable image");
374 }
375 if (image->w > 8192 || image->h > 8192) {
376 image_destroy(image);
377 ffmpeg_decoder_destroy(decoder);
378 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Decoded image dimensions exceed the configured limit");
379 }
380 size_t total = 0;
381 size_t limit = request->max_output_bytes ? request->max_output_bytes : 8u * 1024u * 1024u;
382 for (;;) {
383 if (image->w > 8192 || image->h > 8192) {
384 image_destroy(image);
385 ffmpeg_decoder_destroy(decoder);
386 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Decoded image dimensions exceed the configured limit");
387 }
388 char *ascii = http_renderer_render_image(image, request);
389 image_destroy(image);
390 if (!ascii) {
391 ffmpeg_decoder_destroy(decoder);
392 return SET_ERRNO(ERROR_MEDIA_INIT, "Cannot convert image to ASCII");
393 }
394 size_t length = strlen(ascii);
395 if (length > limit - (total > limit ? limit : total)) {
396 free(ascii);
397 ffmpeg_decoder_destroy(decoder);
398 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Rendered output exceeds the configured limit");
399 }
400 if (http_render_result_add_text_frame(result, ascii, length, (time_get_ns() - started) / 1000000) != ASCIICHAT_OK) {
401 free(ascii);
402 ffmpeg_decoder_destroy(decoder);
403 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Cannot store rendered frame");
404 }
405 if (!result->ascii) {
406 result->ascii = SAFE_MALLOC(length + 1, char *);
407 SAFE_MEMCPY(result->ascii, length + 1, ascii, length + 1);
408 result->ascii_length = length;
409 }
410 total += length;
411 free(ascii);
413 break;
414 if (cancelled && atomic_load_bool(cancelled)) {
415 result->cancelled = true;
416 ffmpeg_decoder_destroy(decoder);
417 return SET_ERRNO(ERROR_INVALID_STATE, "HTTP image render cancelled");
418 }
419 asciichat_error_context_t failure = {0};
420 image = NULL;
421 ffmpeg_frame_status_t status = ffmpeg_decoder_poll_video(decoder, &image, &failure);
422 if (status == FFMPEG_FRAME_EOF)
423 break;
424 if (status == FFMPEG_FRAME_FAILED) {
425 ffmpeg_decoder_destroy(decoder);
426 return SET_ERRNO(ERROR_MEDIA_INIT, "Video frame decoding failed");
427 }
428 while (status == FFMPEG_FRAME_WAITING) {
429 if (time_get_ns() >= deadline) {
430 ffmpeg_decoder_destroy(decoder);
431 return SET_ERRNO(ERROR_MEDIA_DECODE, "Image decoding timed out");
432 }
434 status = ffmpeg_decoder_poll_video(decoder, &image, &failure);
435 }
436 if (!image)
437 break;
438 }
439 ffmpeg_decoder_destroy(decoder);
440 result->elapsed_ms = (time_get_ns() - started) / 1000000;
441 return result->frame_count ? ASCIICHAT_OK : SET_ERRNO(ERROR_MEDIA_INIT, "Renderer returned no frames");
442}
asciichat_error_t errno_import(const asciichat_error_context_t *cause)
#define SET_ERRNO(code,...)
const simd_cells_t * simd_cells_backend(void)
Definition cells.c:70
Error and exit codes - unified status values (0-255).
ffmpeg_decoder_t * ffmpeg_decoder_create_interruptible(const char *path, bool(*cancel)(void *), void *data)
๐ŸŽž๏ธ FFmpeg-based media decoder for video and audio streams
audio_visualization_source_t source
Definition fft.c:24
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#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_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:483
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_BUFFER_FULL
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_DECODE
Definition error_codes.h:67
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
@ ERROR_THREAD
ffmpeg_frame_status_t
Decode next video frame.
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
Destroy FFmpeg decoder and free resources.
ffmpeg_frame_status_t ffmpeg_decoder_poll_video(ffmpeg_decoder_t *decoder, image_t **frame, asciichat_error_context_t *failure)
@ FFMPEG_FRAME_READY
@ FFMPEG_FRAME_WAITING
@ FFMPEG_FRAME_FAILED
@ FFMPEG_FRAME_EOF
#define NS_PER_MS_INT
Definition time.h:168
const char DEFAULT_ASCII_PALETTE[]
Default ASCII palette for legacy functions.
Definition palette.c:31
const palette_def_t * get_builtin_palette(palette_type_t type)
Get built-in palette definition.
Definition palette.c:53
palette_type_t
Built-in palette type enumeration.
Definition palette.h:90
const char * chars
Character sequence (ordered from dark to light).
Definition palette.h:128
@ PALETTE_BLOCKS
Unicode block characters: " โ–‘โ–‘โ–’โ–’โ–“โ–“โ–ˆโ–ˆ".
Definition palette.h:96
@ PALETTE_COOL
Ascending blocks: " โ–โ–‚โ–ƒโ–„โ–…โ–†โ–‡โ–ˆ".
Definition palette.h:102
@ PALETTE_STANDARD
Standard ASCII palette: " ...',;:clodxkO0KXNWM".
Definition palette.h:94
@ PALETTE_DIGITAL
Digital/glitch aesthetic: " -=โ‰กโ‰ฃโ–ฐโ–ฑโ—ผ".
Definition palette.h:98
@ PALETTE_MINIMAL
Simple ASCII: " .-+*#".
Definition palette.h:100
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
char * image_print(const image_t *image, const char *palette)
Print image as ASCII art (monochrome).
Definition foreground.c:12
void image_resize(const image_t *s, image_t *d)
Resize image using nearest-neighbor interpolation.
char * image_print_color_simd(image_t *image, bool background, bool indexed, const char *palette)
Print image as ASCII with color using SIMD.
Definition sgr.c:159
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
asciichat_error_t http_renderer_submit(http_renderer_t *renderer, const http_render_request_t *request, void *user_data, http_renderer_job_t **out_job)
asciichat_error_t http_renderer_render_file(const char *path, const http_render_request_t *request, http_render_result_t *result, atomic_t *cancelled)
void http_render_request_init(http_render_request_t *request)
void http_renderer_job_reset(http_renderer_job_t *job)
void http_renderer_stop(http_renderer_t *renderer)
void http_renderer_job_wait(http_renderer_job_t *job)
void http_renderer_job_join(http_renderer_job_t *job)
void http_renderer_reap(http_renderer_t *renderer, bool all)
asciichat_error_t http_render_result_add_file_frame(http_render_result_t *result, const char *path, uint64_t timestamp_ms)
asciichat_error_t http_renderer_cancel(http_renderer_job_t *job)
asciichat_error_t http_renderer_init(http_renderer_t **out, const http_renderer_config_t *config)
void http_render_result_destroy(http_render_result_t *result)
bool http_renderer_job_is_done(const http_renderer_job_t *job)
asciichat_error_t http_renderer_job_start(http_renderer_job_t *job, const char *name, http_renderer_job_fn execute, void *context)
void http_renderer_job_cancel(http_renderer_job_t *job)
void http_renderer_destroy(http_renderer_t **renderer)
asciichat_error_t http_render_result_add_text_frame(http_render_result_t *result, const char *text, size_t length, uint64_t timestamp_ms)
struct http_renderer_job http_renderer_job_t
#define HTTP_RENDERER_MAX_OUTPUT
void(* http_renderer_job_fn)(void *context, atomic_t *cancelled)
@ HTTP_RENDER_SOURCE_FILE
@ HTTP_RENDER_SOURCE_UPLOAD
@ HTTP_RENDER_SOURCE_URL
@ HTTP_RENDER_OUTPUT_TEXT
#define HTTP_RENDERER_MAX_FRAMES
struct http_renderer http_renderer_t
char * ascii_render_halfblocks(const uint8_t *rgb, int width, int height, int stride, ascii_color_t mode, const simd_cells_t *ops)
char * ascii_render(const image_t *image, const char *palette, ascii_color_t mode, bool background, bool dither, const simd_cells_t *ops)
ASCII Palette Management for Video-to-ASCII Conversion.
static bool size_t gotel_query_t * out
Definition queries.h:8
๐Ÿ”ค String Manipulation and Shell Escaping Utilities
FFmpeg decoder state for video and audio decoding.
char path[PLATFORM_MAX_PATH_LENGTH]
char render_theme[OPTIONS_BUFF_SIZE]
char palette_chars[OPTIONS_BUFF_SIZE]
char palette[OPTIONS_BUFF_SIZE]
http_render_source_t source
http_render_output_type_t output_type
http_render_options_t options
http_render_frame_t * frames
asciichat_error_t error
char output_path[PLATFORM_MAX_PATH_LENGTH]
char path[PLATFORM_MAX_PATH_LENGTH]
http_render_source_type_t type
char url[OPTIONS_BUFF_SIZE]
void(* on_complete)(http_renderer_job_t *job, const http_render_result_t *result, void *user_data)
void(* on_error)(http_renderer_job_t *job, asciichat_error_t error, void *user_data)
http_renderer_t * renderer
http_render_result_t result
http_renderer_job_fn execute
http_render_request_t request
platform_thread_t thread
struct http_renderer_job * next
http_renderer_job_t * jobs
http_renderer_config_t config
unsigned active_jobs
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
int w
Image width in pixels (must be > 0).
int h
Image height in pixels (must be > 0).
rgb_pixel_t * pixels
Pixel data array (width * height RGB pixels, row-major order).
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash