AtlatestRepositorysigil-graphics

sigil-graphics / tree / src / cgraphics.c

1/*
2 * graphics.c - Sigil Graphics Module
3 *
4 * Wraps sokol_gfx.h to provide 2D/3D rendering capabilities.
5 */
6
7#include "sigil-internal.h"
8
9/* Sokol headers (implementation is in sokol-graphics.c).
10 *
11 * The window is a platform concern this file reaches only through the
12 * sig_gfx_ abstraction below: natively sigil-desktop's C surface
13 * (framebuffer size; the GL context is current from the init callback on),
14 * on web (wasm32-wasi) the sigil-wasm-gles3 JS bridge (canvas + WebGL2
15 * context + swapchain). Both build the sokol_gfx environment and swapchain
16 * by hand; there is no sokol_glue. */
17#include "sokol_gfx.h"
18#include "sokol_gp.h"
19#include "sokol_log.h"
20#if !defined(__wasm__)
21#include "sigil-desktop.h"
22#endif
24#include <stdio.h>
25#include <stdlib.h>
26#include <stdbool.h>
27#include <string.h>
28#include <math.h>
29#include <time.h>
30#include <stdint.h>
32/* ---- platform abstraction: native (sigil-desktop) vs web (GL bridge) --------
33 * These four accessors are the ONLY window touch points in this file. Web
34 * supplies the default framebuffer swapchain manually (lifted from the
35 * Milestone-1 wasm-sokol-sprite example) and sources the canvas size from
36 * the sigil_wasm_gles3 app-shell import module; native asks sigil-desktop
37 * for the framebuffer size and reports the same formats sokol_app did. */
38#if defined(__wasm__)
39__attribute__((import_module("sigil_wasm_gles3"), import_name("canvas_width")))
40extern int sigil_wasm_gles3_canvas_width(void);
41__attribute__((import_module("sigil_wasm_gles3"), import_name("canvas_height")))
42extern int sigil_wasm_gles3_canvas_height(void);
44static int sig_gfx_width(void) { return sigil_wasm_gles3_canvas_width(); }
45static int sig_gfx_height(void) { return sigil_wasm_gles3_canvas_height(); }
47static sg_environment sig_gfx_environment(void) {
48 sg_environment env = {0};
49 env.defaults.color_format = SG_PIXELFORMAT_RGBA8;
50 env.defaults.depth_format = SG_PIXELFORMAT_NONE;
51 env.defaults.sample_count = 1;
52 return env;
54static sg_swapchain sig_gfx_swapchain(void) {
55 sg_swapchain swap = {0};
56 swap.width = sig_gfx_width();
57 swap.height = sig_gfx_height();
58 swap.sample_count = 1;
59 swap.color_format = SG_PIXELFORMAT_RGBA8;
60 swap.depth_format = SG_PIXELFORMAT_NONE;
61 swap.gl.framebuffer = 0; /* default framebuffer */
62 return swap;
64#else
65/* Native: sigil-desktop's framebuffer, and the values sokol_app's GL
66 * backend used to report (RGBA8, a combined 24/8 depth-stencil buffer,
67 * which is GLFW's default framebuffer, no MSAA, GL framebuffer 0). */
68static int sig_gfx_width(void) { int w = 1, h = 1; sigil_desktop_framebuffer_size(&w, &h); return w; }
69static int sig_gfx_height(void) { int w = 1, h = 1; sigil_desktop_framebuffer_size(&w, &h); return h; }
71static sg_environment sig_gfx_environment(void) {
72 sg_environment env = {0};
73 env.defaults.color_format = SG_PIXELFORMAT_RGBA8;
74 env.defaults.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL;
75 env.defaults.sample_count = 1;
76 return env;
78static sg_swapchain sig_gfx_swapchain(void) {
79 sg_swapchain swap = {0};
80 swap.width = sig_gfx_width();
81 swap.height = sig_gfx_height();
82 swap.sample_count = 1;
83 swap.color_format = SG_PIXELFORMAT_RGBA8;
84 swap.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL;
85 swap.gl.framebuffer = 0;
86 return swap;
89/* sokol-graphics.c: fill sokol's GL entry points from the current context. */
90int sigil_graphics_load_gl(void);
91#endif
93#ifndef M_PI
94#define M_PI 3.14159265358979323846
95#endif
97/* Maximum segments for a circle triangle fan. Stack-allocated buffer is sized
98 * to this. Higher values produce smoother circles at the cost of more triangles
99 * per draw call. 64 is plenty for typical bullet/UI usage. */
100#define SIGIL_GFX_CIRCLE_MAX_SEGMENTS 128
102/* External: get pixel data from image (defined in image.c) */
103extern unsigned char *sigil_graphics_image_pixels(SigilVM *vm, Value img_val, int *width, int *height);
105/* Graphics initialized flag */
106static bool gfx_initialized = false;
108/* Texture type tag (initialized at module init) */
109static Value texture_type_tag = SIGIL_UNDEFINED;
111/* Render target type tag (initialized at module init) */
112static Value rt_type_tag = SIGIL_UNDEFINED;
114/* Shader type tag (initialized at module init) */
115static Value shader_type_tag = SIGIL_UNDEFINED;
117/* Texture structure.
118 *
119 * `owns_resources` is false for textures that borrow their handles from
120 * another owner (e.g., from a render target via render-target->texture).
121 * Borrowed wrappers must not destroy the underlying sokol resources;
122 * the original owner does that. */
123typedef struct {
124 sg_image handle;
125 sg_sampler sampler;
126 sg_view view;
127 int width;
128 int height;
129 bool owns_resources;
130 /* make-texture-from-pixels creates the image with dynamic_update
131 * usage so update-texture can refill it. sokol_gfx allows one
132 * sg_update_image per image per frame; `upload_frame` is the
133 * gfx_frame_serial of the last upload so update-texture can refuse
134 * a second one instead of tripping sokol's validation layer. */
135 bool dynamic;
136 uint32_t upload_frame;
137} GfxTexture;
139/* Frame serial: advanced by end-frame right after sg_commit, so it
140 * moves in lockstep with sokol_gfx's private frame index (which is what
141 * sg_update_image's once-per-frame rule is checked against). Starts at
142 * 1 like sokol's. */
143static uint32_t gfx_frame_serial = 1;
145/* Render target structure.
146 *
147 * Holds a color image, a color-attachment view (for rendering INTO),
148 * a texture view (for sampling AS texture), a sampler, and a
149 * depth-stencil image + view. The depth-stencil attachment is only
150 * needed because sgp's default pipelines bake in the swap chain's
151 * depth-stencil format; offscreen passes must provide a matching
152 * attachment so pipeline validation passes. We don't actually use the
153 * depth buffer for 2D rendering. */
154typedef struct {
155 sg_image color_img;
156 sg_view color_att_view;
157 sg_view tex_view;
158 sg_sampler sampler;
159 sg_image depth_img;
160 sg_view depth_att_view;
161 int width;
162 int height;
163 bool freed;
164} GfxRenderTarget;
166/* Shader uniform entry — one per uniform declared in the user's
167 * fragment GLSL. The `offset` is into the per-shader uniform buffer;
168 * sgp_set_uniform ships the buffer contents to the GPU per-draw.
169 *
170 * SIGIL_GFX_UNIFORM_BUFFER_SIZE must equal SGP_UNIFORM_CONTENT_SLOTS *
171 * sizeof(float) (see sokol-graphics.c): sgp_set_uniform asserts the
172 * pushed size fits its per-draw slot. 512 B holds `vec4 name[8]` arrays four
173 * times over beside the auto-uniforms; a larger block costs sokol_gp
174 * `max_commands` (16384) copies of it in its uniform pool (David, 2026-09-19:
175 * 512 B over 1 KiB, an 8.4 MB pool over 16.8 MB).
176 *
177 * sokol_gfx caps a uniform block at SG_MAX_UNIFORMBLOCK_MEMBERS (16)
178 * declared members; an array counts as one member, so the 16 here is
179 * that cap, not a float budget. */
180#define SIGIL_GFX_MAX_UNIFORMS 16
181#define SIGIL_GFX_UNIFORM_BUFFER_SIZE 512 /* matches SGP_UNIFORM_CONTENT_SLOTS=128 floats */
182#define SIGIL_GFX_UNIFORM_NAME_MAX 64
184typedef struct {
185 char name[SIGIL_GFX_UNIFORM_NAME_MAX];
186 sg_uniform_type type;
187 uint32_t offset;
188 uint32_t size; /* total bytes = elem_size * array_count */
189 uint32_t elem_size; /* bytes of one element (float=4 ... mat4=64) */
190 int array_count; /* 1 for a scalar/vector, N for `name[N]` */
191} GfxUniformEntry;
193/* Shader structure.
194 *
195 * Holds the compiled sokol shader, the sgp pipeline that bakes it in,
196 * the parsed uniform layout (name → offset/type/size), and a CPU
197 * buffer that mirrors the GPU uniform block. Auto-uniforms u_time and
198 * u_resolution are tracked by index for fast per-draw refresh.
199 *
200 * Texture sampler bindings (sampler2D uniforms) are NOT tracked here —
201 * channel 0 is bound by the draw primitive itself (e.g.,
202 * draw-render-target sets channel 0 to the source texture). */
203typedef struct {
204 sg_shader shader;
205 sg_pipeline pipeline;
206 GfxUniformEntry uniforms[SIGIL_GFX_MAX_UNIFORMS];
207 int num_uniforms;
208 uint8_t buffer[SIGIL_GFX_UNIFORM_BUFFER_SIZE];
209 uint32_t buffer_size;
210 int u_time_index; /* -1 if shader doesn't use u_time */
211 int u_resolution_index; /* -1 if shader doesn't use u_resolution */
212 bool freed;
213} GfxShader;
215/* Pointer to the currently active shader (set by %bind-shader,
216 * cleared by %unbind-shader). Used by the auto-uniform refresh path
217 * during draw-render-target / draw-texture so u_time advances and
218 * u_resolution tracks viewport size without caller intervention. */
219static GfxShader *active_shader = NULL;
221/* The render-target pass in progress, if any (%begin-rt-pass sets it,
222 * %end-rt-pass clears it; sokol_gfx passes do not nest). While active,
223 * the auto-uniform u_resolution reports this size. */
224static bool rt_pass_active = false;
225static int rt_pass_width = 0;
226static int rt_pass_height = 0;
228/* Process start time for u_time. Set on first sg_setup. */
229static double gfx_start_time = 0.0;
231/* Current draw color */
232static float draw_color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
234/* Clear color (set by clear, used in end-frame) */
235static float clear_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
237/* SGP initialized flag */
238static bool sgp_initialized = false;
240/* Virtual viewport state */
241static bool virtual_viewport_enabled = false;
242static int virtual_width = 0;
243static int virtual_height = 0;
245/* Letterbox color (bars outside viewport) */
246static float letterbox_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
248/* Helper to extract float from fixnum or flonum */
249static float value_to_float(Value v)
251 if (sigil_is_fixnum(v)) {
252 return (float)sigil_as_fixnum(v);
253 } else if (sigil_is_flonum(v)) {
254 return (float)sigil_as_flonum(v);
255 }
256 return 0.0f;
259/* Monotonic seconds since gfx-setup. Used by the shader auto-uniform
260 * u_time. clock_gettime(CLOCK_MONOTONIC) is unaffected by wall-clock
261 * jumps and has nanosecond resolution. */
262static float gfx_elapsed_seconds(void)
264 struct timespec ts;
265 clock_gettime(CLOCK_MONOTONIC, &ts);
266 double now = (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
267 return (float)(now - gfx_start_time);
270/* ============================================================
271 * NATIVE FUNCTIONS
272 * ============================================================ */
274/*
275 * (gfx-setup) - Initialize graphics subsystem
276 * Must be called in the app-run init callback after window is created.
277 */
278static Value native_gfx_setup(SigilVM *vm, int argc, Value *args)
280 (void)vm; (void)argc; (void)args;
282 if (gfx_initialized) {
283 return SIGIL_NIL;
284 }
286#if !defined(__wasm__)
287 /* Fill sokol's GL entry points from the window's context, which
288 * sigil-desktop made current before the init callback ran. Without a
289 * context (gfx-setup outside app-run) every pointer stays NULL and
290 * sg_setup would crash on the first call, so refuse here instead. */
291 {
292 int missing = sigil_graphics_load_gl();
293 if (missing) {
294 sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
295 "gfx-setup: no OpenGL context; call it from the game thunk (inside run-game / app-run)");
296 return SIGIL_UNDEFINED;
297 }
298 }
299#endif
301 /* Initialize sokol_gfx. Install slog_func so validation failures
302 * print a useful diagnostic before sokol aborts. */
303 sg_desc desc = {
304 .environment = sig_gfx_environment(),
305 .logger.func = slog_func,
306 };
307 sg_setup(&desc);
309 /* Capture process start time for the shader u_time auto-uniform. */
310 {
311 struct timespec ts;
312 clock_gettime(CLOCK_MONOTONIC, &ts);
313 gfx_start_time = (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
314 }
316 /* Initialize sokol_gp for 2D rendering */
317 sgp_desc sgpdesc = {0};
318 sgp_setup(&sgpdesc);
319 if (!sgp_is_valid()) {
320 fprintf(stderr, "Failed to initialize sokol_gp\n");
321 sg_shutdown();
322 return SIGIL_FALSE;
323 }
324 sgp_initialized = true;
325 gfx_initialized = true;
327 return SIGIL_NIL;
330/*
331 * (gfx-shutdown) - Shutdown graphics subsystem
332 */
333static Value native_gfx_shutdown(SigilVM *vm, int argc, Value *args)
335 (void)vm; (void)argc; (void)args;
337 if (gfx_initialized) {
338 if (sgp_initialized) {
339 sgp_shutdown();
340 sgp_initialized = false;
341 }
342 sg_shutdown();
343 gfx_initialized = false;
344 }
346 return SIGIL_NIL;
349/*
350 * (set-letterbox-color r g b [a]) - Set the color for letterbox bars
351 *
352 * Default is black. Only visible when using a virtual viewport.
353 */
354static Value native_set_letterbox_color(SigilVM *vm, int argc, Value *args)
356 if (argc < 3) {
357 sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-letterbox-color: requires r, g, b arguments");
358 return SIGIL_UNDEFINED;
359 }
361 letterbox_color[0] = value_to_float(args[0]);
362 letterbox_color[1] = value_to_float(args[1]);
363 letterbox_color[2] = value_to_float(args[2]);
364 letterbox_color[3] = argc > 3 ? value_to_float(args[3]) : 1.0f;
366 return SIGIL_NIL;
369/*
370 * (set-viewport width height) - Set virtual viewport with letterboxing
371 *
372 * Creates a fixed coordinate space that maintains aspect ratio.
373 * Black bars are added as needed to fill the window.
374 * Call with #f to disable and use window coordinates.
375 */
376static Value native_set_viewport(SigilVM *vm, int argc, Value *args)
378 (void)vm;
380 if (argc == 1 && sigil_is_false(args[0])) {
381 /* Disable virtual viewport */
382 virtual_viewport_enabled = false;
383 return SIGIL_NIL;
384 }
386 if (argc < 2) {
387 sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-viewport: requires width, height");
388 return SIGIL_UNDEFINED;
389 }
391 virtual_viewport_enabled = true;
392 virtual_width = (int)value_to_float(args[0]);
393 virtual_height = (int)value_to_float(args[1]);
395 return SIGIL_NIL;
398/*
399 * (begin-frame) - Begin a new frame
400 */
401static Value native_begin_frame(SigilVM *vm, int argc, Value *args)
403 (void)vm; (void)argc; (void)args;
405 int window_w = sig_gfx_width();
406 int window_h = sig_gfx_height();
408 /* Begin sokol_gp frame with full window size */
409 sgp_begin(window_w, window_h);
411 if (virtual_viewport_enabled && virtual_width > 0 && virtual_height > 0) {
412 /* Calculate letterbox viewport */
413 float scale_x = (float)window_w / (float)virtual_width;
414 float scale_y = (float)window_h / (float)virtual_height;
415 float scale = (scale_x < scale_y) ? scale_x : scale_y;
417 int viewport_w = (int)(virtual_width * scale);
418 int viewport_h = (int)(virtual_height * scale);
419 int viewport_x = (window_w - viewport_w) / 2;
420 int viewport_y = (window_h - viewport_h) / 2;
422 sgp_viewport(viewport_x, viewport_y, viewport_w, viewport_h);
423 sgp_project(0, (float)virtual_width, 0, (float)virtual_height);
424 } else {
425 /* Default: use window coordinates */
426 sgp_viewport(0, 0, window_w, window_h);
427 sgp_project(0, (float)window_w, 0, (float)window_h);
428 }
430 /* Reset to white draw color */
431 sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
433 return SIGIL_NIL;
436/*
437 * (end-frame) - End the current frame
438 */
439static Value native_end_frame(SigilVM *vm, int argc, Value *args)
441 (void)vm; (void)argc; (void)args;
443 /* Begin render pass - clear to letterbox color */
444 sg_pass_action pass_action = {
445 .colors[0] = {
446 .load_action = SG_LOADACTION_CLEAR,
447 .clear_value = {letterbox_color[0], letterbox_color[1],
448 letterbox_color[2], letterbox_color[3]}
449 }
450 };
451 sg_pass pass = {
452 .action = pass_action,
453 .swapchain = sig_gfx_swapchain()
454 };
455 sg_begin_pass(&pass);
457 /* Flush sokol_gp commands to GPU */
458 sgp_flush();
459 sgp_end();
461 sg_end_pass();
462 sg_commit();
463 gfx_frame_serial++;
465 return SIGIL_NIL;
468/*
469 * (clear-screen r g b [a]) - Clear the viewport with a color
470 *
471 * When using a virtual viewport, this fills the viewport area.
472 * The letterbox bars remain the pass clear color (black).
473 */
474static Value native_clear_screen(SigilVM *vm, int argc, Value *args)
476 if (argc < 3) {
477 sigil__vm_error(vm, SIGIL_ERR_ARITY, "clear-screen: requires r, g, b arguments");
478 return SIGIL_UNDEFINED;
479 }
481 float r = value_to_float(args[0]);
482 float g = value_to_float(args[1]);
483 float b = value_to_float(args[2]);
484 float a = argc > 3 ? value_to_float(args[3]) : 1.0f;
486 /* Draw a filled rectangle covering the entire viewport/projection area */
487 sgp_set_color(r, g, b, a);
488 if (virtual_viewport_enabled && virtual_width > 0 && virtual_height > 0) {
489 sgp_draw_filled_rect(0, 0, (float)virtual_width, (float)virtual_height);
490 } else {
491 sgp_draw_filled_rect(0, 0, (float)sig_gfx_width(), (float)sig_gfx_height());
492 }
494 /* Reset to white for subsequent drawing */
495 sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
497 return SIGIL_NIL;
500/*
501 * (set-color r g b [a]) - Set current draw color
502 */
503static Value native_set_color(SigilVM *vm, int argc, Value *args)
505 if (argc < 3) {
506 sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-color: requires r, g, b arguments");
507 return SIGIL_UNDEFINED;
508 }
510 draw_color[0] = value_to_float(args[0]);
511 draw_color[1] = value_to_float(args[1]);
512 draw_color[2] = value_to_float(args[2]);
513 draw_color[3] = argc > 3 ? value_to_float(args[3]) : 1.0f;
515 /* Set sokol_gp color */
516 sgp_set_color(draw_color[0], draw_color[1], draw_color[2], draw_color[3]);
518 return SIGIL_NIL;
521/*
522 * (draw-filled-rect x y w h) - Draw a filled rectangle
523 */
524static Value native_draw_filled_rect(SigilVM *vm, int argc, Value *args)
526 if (argc < 4) {
527 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-filled-rect: requires x, y, w, h arguments");
528 return SIGIL_UNDEFINED;
529 }
531 float x = value_to_float(args[0]);
532 float y = value_to_float(args[1]);
533 float w = value_to_float(args[2]);
534 float h = value_to_float(args[3]);
536 sgp_draw_filled_rect(x, y, w, h);
538 return SIGIL_NIL;
541/*
542 * (draw-rect x y w h) - Draw a rectangle outline
543 */
544static Value native_draw_rect(SigilVM *vm, int argc, Value *args)
546 if (argc < 4) {
547 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-rect: requires x, y, w, h arguments");
548 return SIGIL_UNDEFINED;
549 }
551 float x = value_to_float(args[0]);
552 float y = value_to_float(args[1]);
553 float w = value_to_float(args[2]);
554 float h = value_to_float(args[3]);
556 /* Draw rectangle outline using 4 lines */
557 sgp_line lines[4] = {
558 {{x, y}, {x + w, y}}, /* top */
559 {{x + w, y}, {x + w, y + h}}, /* right */
560 {{x + w, y + h}, {x, y + h}}, /* bottom */
561 {{x, y + h}, {x, y}} /* left */
562 };
563 sgp_draw_lines(lines, 4);
565 return SIGIL_NIL;
568/*
569 * (draw-line x1 y1 x2 y2) - Draw a line
570 */
571static Value native_draw_line(SigilVM *vm, int argc, Value *args)
573 if (argc < 4) {
574 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-line: requires x1, y1, x2, y2 arguments");
575 return SIGIL_UNDEFINED;
576 }
578 float x1 = value_to_float(args[0]);
579 float y1 = value_to_float(args[1]);
580 float x2 = value_to_float(args[2]);
581 float y2 = value_to_float(args[3]);
583 sgp_line line = {{x1, y1}, {x2, y2}};
584 sgp_draw_lines(&line, 1);
586 return SIGIL_NIL;
589/*
590 * (draw-point x y) - Draw a single point
591 */
592static Value native_draw_point(SigilVM *vm, int argc, Value *args)
594 if (argc < 2) {
595 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-point: requires x, y arguments");
596 return SIGIL_UNDEFINED;
597 }
599 float x = value_to_float(args[0]);
600 float y = value_to_float(args[1]);
602 sgp_point pt = {x, y};
603 sgp_draw_points(&pt, 1);
605 return SIGIL_NIL;
608/*
609 * (draw-triangle x1 y1 x2 y2 x3 y3) - Draw a triangle outline
610 */
611static Value native_draw_triangle(SigilVM *vm, int argc, Value *args)
613 if (argc < 6) {
614 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-triangle: requires x1, y1, x2, y2, x3, y3");
615 return SIGIL_UNDEFINED;
616 }
618 float x1 = value_to_float(args[0]);
619 float y1 = value_to_float(args[1]);
620 float x2 = value_to_float(args[2]);
621 float y2 = value_to_float(args[3]);
622 float x3 = value_to_float(args[4]);
623 float y3 = value_to_float(args[5]);
625 sgp_line lines[3] = {
626 {{x1, y1}, {x2, y2}},
627 {{x2, y2}, {x3, y3}},
628 {{x3, y3}, {x1, y1}}
629 };
630 sgp_draw_lines(lines, 3);
632 return SIGIL_NIL;
635/*
636 * (fill-triangle x1 y1 x2 y2 x3 y3) - Draw a filled triangle
637 */
638static Value native_fill_triangle(SigilVM *vm, int argc, Value *args)
640 if (argc < 6) {
641 sigil__vm_error(vm, SIGIL_ERR_ARITY, "fill-triangle: requires x1, y1, x2, y2, x3, y3");
642 return SIGIL_UNDEFINED;
643 }
645 float x1 = value_to_float(args[0]);
646 float y1 = value_to_float(args[1]);
647 float x2 = value_to_float(args[2]);
648 float y2 = value_to_float(args[3]);
649 float x3 = value_to_float(args[4]);
650 float y3 = value_to_float(args[5]);
652 sgp_triangle tri = {{x1, y1}, {x2, y2}, {x3, y3}};
653 sgp_draw_filled_triangles(&tri, 1);
655 return SIGIL_NIL;
658/*
659 * (draw-filled-circle x y radius [segments]) - Draw a filled circle
660 *
661 * Renders the circle as a triangle fan around (x, y). `segments` controls
662 * the polygon resolution; default is 16. For tiny bullets (radius 4-6 px),
663 * 12-16 is plenty. Larger circles benefit from more segments. Clamped to
664 * [3, SIGIL_GFX_CIRCLE_MAX_SEGMENTS].
665 */
666static Value native_draw_filled_circle(SigilVM *vm, int argc, Value *args)
668 if (argc < 3) {
669 sigil__vm_error(vm, SIGIL_ERR_ARITY,
670 "draw-filled-circle: requires x, y, radius arguments");
671 return SIGIL_UNDEFINED;
672 }
674 float cx = value_to_float(args[0]);
675 float cy = value_to_float(args[1]);
676 float radius = value_to_float(args[2]);
678 int segments = 16;
679 if (argc > 3) {
680 segments = (int)value_to_float(args[3]);
681 }
682 if (segments < 3) segments = 3;
683 if (segments > SIGIL_GFX_CIRCLE_MAX_SEGMENTS) {
684 segments = SIGIL_GFX_CIRCLE_MAX_SEGMENTS;
685 }
687 if (radius <= 0.0f) {
688 return SIGIL_NIL;
689 }
691 sgp_triangle tris[SIGIL_GFX_CIRCLE_MAX_SEGMENTS];
692 float step = (float)(2.0 * M_PI) / (float)segments;
693 float prev_x = cx + radius;
694 float prev_y = cy;
695 for (int i = 1; i <= segments; ++i) {
696 float angle = step * (float)i;
697 float nx = cx + radius * cosf(angle);
698 float ny = cy + radius * sinf(angle);
699 tris[i - 1].a.x = cx; tris[i - 1].a.y = cy;
700 tris[i - 1].b.x = prev_x; tris[i - 1].b.y = prev_y;
701 tris[i - 1].c.x = nx; tris[i - 1].c.y = ny;
702 prev_x = nx;
703 prev_y = ny;
704 }
706 sgp_draw_filled_triangles(tris, (uint32_t)segments);
708 return SIGIL_NIL;
711/* ============================================================
712 * BLEND MODES
713 * ============================================================ */
715/* Map a Sigil symbol value to an sgp_blend_mode. Returns -1 if unknown. */
716static int blend_mode_from_value(Value v)
718 if (sigil_is_symbol(v)) {
719 const char *name = sigil_symbol_name(v);
720 if (name) {
721 if (strcmp(name, "normal") == 0) return SGP_BLENDMODE_BLEND;
722 if (strcmp(name, "additive") == 0) return SGP_BLENDMODE_ADD;
723 if (strcmp(name, "none") == 0) return SGP_BLENDMODE_NONE;
724 }
725 }
726 return -1;
729/*
730 * (set-blend-mode mode) - Set the current blend mode
731 *
732 * mode is one of: 'normal (alpha blend), 'additive, 'none.
733 * Stays in effect until changed or reset-blend-mode is called.
734 */
735static Value native_set_blend_mode(SigilVM *vm, int argc, Value *args)
737 if (argc < 1) {
738 sigil__vm_error(vm, SIGIL_ERR_ARITY,
739 "set-blend-mode: requires mode symbol");
740 return SIGIL_UNDEFINED;
741 }
742 int mode = blend_mode_from_value(args[0]);
743 if (mode < 0) {
744 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
745 "set-blend-mode: expected 'normal, 'additive, or 'none");
746 return SIGIL_UNDEFINED;
747 }
748 sgp_set_blend_mode((sgp_blend_mode)mode);
749 return SIGIL_NIL;
752/*
753 * (reset-blend-mode) - Reset blend mode to sokol_gp default (no blending)
754 */
755static Value native_reset_blend_mode(SigilVM *vm, int argc, Value *args)
757 (void)vm; (void)argc; (void)args;
758 sgp_reset_blend_mode();
759 return SIGIL_NIL;
762/* ============================================================
763 * TRANSFORM STACK
764 * ============================================================ */
766/*
767 * (push-transform) - Save current transform state
768 */
769static Value native_push_transform(SigilVM *vm, int argc, Value *args)
771 (void)vm; (void)argc; (void)args;
772 sgp_push_transform();
773 return SIGIL_NIL;
776/*
777 * (pop-transform) - Restore previous transform state
778 */
779static Value native_pop_transform(SigilVM *vm, int argc, Value *args)
781 (void)vm; (void)argc; (void)args;
782 sgp_pop_transform();
783 return SIGIL_NIL;
786/*
787 * (reset-transform) - Reset to identity transform
788 */
789static Value native_reset_transform(SigilVM *vm, int argc, Value *args)
791 (void)vm; (void)argc; (void)args;
792 sgp_reset_transform();
793 return SIGIL_NIL;
796/*
797 * (translate x y) - Translate by (x, y)
798 */
799static Value native_translate(SigilVM *vm, int argc, Value *args)
801 if (argc < 2) {
802 sigil__vm_error(vm, SIGIL_ERR_ARITY, "translate: requires x, y arguments");
803 return SIGIL_UNDEFINED;
804 }
806 float x = value_to_float(args[0]);
807 float y = value_to_float(args[1]);
809 sgp_translate(x, y);
811 return SIGIL_NIL;
814/*
815 * (rotate angle) - Rotate by angle (in radians)
816 */
817static Value native_rotate(SigilVM *vm, int argc, Value *args)
819 if (argc < 1) {
820 sigil__vm_error(vm, SIGIL_ERR_ARITY, "rotate: requires angle argument");
821 return SIGIL_UNDEFINED;
822 }
824 float angle = value_to_float(args[0]);
825 sgp_rotate(angle);
827 return SIGIL_NIL;
830/*
831 * (rotate-at angle x y) - Rotate around point (x, y)
832 */
833static Value native_rotate_at(SigilVM *vm, int argc, Value *args)
835 if (argc < 3) {
836 sigil__vm_error(vm, SIGIL_ERR_ARITY, "rotate-at: requires angle, x, y arguments");
837 return SIGIL_UNDEFINED;
838 }
840 float angle = value_to_float(args[0]);
841 float x = value_to_float(args[1]);
842 float y = value_to_float(args[2]);
844 sgp_rotate_at(angle, x, y);
846 return SIGIL_NIL;
849/*
850 * (scale sx sy) - Scale by (sx, sy)
851 */
852static Value native_scale(SigilVM *vm, int argc, Value *args)
854 if (argc < 2) {
855 sigil__vm_error(vm, SIGIL_ERR_ARITY, "scale: requires sx, sy arguments");
856 return SIGIL_UNDEFINED;
857 }
859 float sx = value_to_float(args[0]);
860 float sy = value_to_float(args[1]);
862 sgp_scale(sx, sy);
864 return SIGIL_NIL;
867/*
868 * (scale-at sx sy x y) - Scale around point (x, y)
869 */
870static Value native_scale_at(SigilVM *vm, int argc, Value *args)
872 if (argc < 4) {
873 sigil__vm_error(vm, SIGIL_ERR_ARITY, "scale-at: requires sx, sy, x, y arguments");
874 return SIGIL_UNDEFINED;
875 }
877 float sx = value_to_float(args[0]);
878 float sy = value_to_float(args[1]);
879 float x = value_to_float(args[2]);
880 float y = value_to_float(args[3]);
882 sgp_scale_at(sx, sy, x, y);
884 return SIGIL_NIL;
887/*
888 * (gfx-initialized?) -> boolean
889 */
890static Value native_gfx_initialized(SigilVM *vm, int argc, Value *args)
892 (void)vm; (void)argc; (void)args;
893 return gfx_initialized ? SIGIL_TRUE : SIGIL_FALSE;
896/* ============================================================
897 * TEXTURE FUNCTIONS
898 * ============================================================ */
900/* Initialize texture type tag */
901static void ensure_texture_type(SigilVM *vm)
903 if (sigil_is_undefined(texture_type_tag)) {
904 texture_type_tag = sigil_intern_symbol(vm, "sigil-graphics-texture", 22);
905 }
908/* Get texture from Value, returns NULL if not a texture */
909static GfxTexture *get_texture(SigilVM *vm, Value v)
911 if (!sigil_is_foreign(v)) return NULL;
912 ensure_texture_type(vm);
913 if (sigil_foreign_type(v) != texture_type_tag) return NULL;
914 return (GfxTexture *)sigil_foreign_data(v);
917/* Texture destructor */
918static void texture_destructor(void *data)
920 GfxTexture *tex = (GfxTexture *)data;
921 if (tex) {
922 if (tex->owns_resources && sg_isvalid()) {
923 if (tex->view.id != SG_INVALID_ID) {
924 sg_destroy_view(tex->view);
925 }
926 if (tex->handle.id != SG_INVALID_ID) {
927 sg_destroy_image(tex->handle);
928 }
929 if (tex->sampler.id != SG_INVALID_ID) {
930 sg_destroy_sampler(tex->sampler);
931 }
932 }
933 free(tex);
934 }
937/*
938 * (load-texture image) -> <texture> or #f
939 *
940 * Create a GPU texture from a CPU-side image.
941 */
942static Value native_load_texture(SigilVM *vm, int argc, Value *args)
944 (void)argc;
946 int width, height;
947 unsigned char *pixels = sigil_graphics_image_pixels(vm, args[0], &width, &height);
949 if (!pixels) {
950 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
951 "load-texture: expected valid image with pixel data");
952 return SIGIL_FALSE;
953 }
955 /* Create sokol image */
956 sg_image_desc img_desc = {
957 .width = width,
958 .height = height,
959 .pixel_format = SG_PIXELFORMAT_RGBA8,
960 .data.mip_levels[0] = {
961 .ptr = pixels,
962 .size = (size_t)(width * height * 4)
963 }
964 };
965 sg_image img = sg_make_image(&img_desc);
967 if (img.id == SG_INVALID_ID || sg_query_image_state(img) != SG_RESOURCESTATE_VALID) {
968 if (img.id != SG_INVALID_ID) sg_destroy_image(img); /* a FAILED slot still holds an id */
969 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
970 "load-texture: failed to create GPU texture");
971 return SIGIL_FALSE;
972 }
974 /* Create sampler with default settings (linear filtering, clamp) */
975 sg_sampler_desc smp_desc = {
976 .min_filter = SG_FILTER_LINEAR,
977 .mag_filter = SG_FILTER_LINEAR,
978 .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
979 .wrap_v = SG_WRAP_CLAMP_TO_EDGE
980 };
981 sg_sampler smp = sg_make_sampler(&smp_desc);
983 /* Create texture view from image (required by new sokol_gp API) */
984 sg_view view = sgp_make_texture_view_from_image(img, "sigil-texture");
985 if (view.id == SG_INVALID_ID || sg_query_view_state(view) != SG_RESOURCESTATE_VALID) {
986 if (view.id != SG_INVALID_ID) sg_destroy_view(view);
987 sg_destroy_image(img);
988 sg_destroy_sampler(smp);
989 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
990 "load-texture: failed to create texture view");
991 return SIGIL_FALSE;
992 }
994 /* Create texture structure */
995 GfxTexture *tex = malloc(sizeof(GfxTexture));
996 if (!tex) {
997 sg_destroy_view(view);
998 sg_destroy_image(img);
999 sg_destroy_sampler(smp);
1000 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "load-texture: out of memory");
1001 return SIGIL_FALSE;
1004 tex->handle = img;
1005 tex->sampler = smp;
1006 tex->view = view;
1007 tex->width = width;
1008 tex->height = height;
1009 tex->owns_resources = true;
1010 tex->dynamic = false;
1011 tex->upload_frame = 0;
1013 ensure_texture_type(vm);
1014 return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
1015 sizeof(GfxTexture));
1018/* Parse an optional sampler-filter argument: the symbol `nearest` or
1019 * `linear` (a string is accepted too). Returns 0 and sets the VM error
1020 * on anything else. `who` names the caller for the message. */
1021static int filter_from_value(SigilVM *vm, Value v, const char *who, sg_filter *out)
1023 const char *name = NULL;
1024 if (sigil_is_symbol(v)) {
1025 name = sigil_symbol_name(v);
1026 } else if (sigil_is_string(v)) {
1027 name = sigil_string_bytes(v);
1029 if (name && strcmp(name, "nearest") == 0) { *out = SG_FILTER_NEAREST; return 1; }
1030 if (name && strcmp(name, "linear") == 0) { *out = SG_FILTER_LINEAR; return 1; }
1031 char msg[160];
1032 snprintf(msg, sizeof(msg), "%s: filter must be 'nearest or 'linear", who);
1033 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, msg);
1034 return 0;
1037/* Bytevector argument check shared by the pixel-upload natives: the
1038 * value must be a bytevector holding exactly width*height*4 bytes
1039 * (RGBA8, row-major, top row first). Returns the byte pointer or NULL
1040 * with the VM error set. */
1041static const uint8_t *pixels_from_bytevector(SigilVM *vm, Value v, int width,
1042 int height, const char *who)
1044 if (!sigil_is_bytevector(v)) {
1045 char msg[160];
1046 snprintf(msg, sizeof(msg), "%s: pixels must be a bytevector", who);
1047 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, msg);
1048 return NULL;
1050 size_t need = (size_t)width * (size_t)height * 4u;
1051 size_t got = sigil_bytevector_length(v);
1052 if (got != need) {
1053 char msg[200];
1054 snprintf(msg, sizeof(msg),
1055 "%s: pixel size mismatch: %dx%d RGBA8 needs %zu bytes, got %zu",
1056 who, width, height, need, got);
1057 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, msg);
1058 return NULL;
1060 return sigil_bytevector_data(v);
1064 * (make-texture-from-pixels width height bytes [filter]) -> <texture>
1066 * Create a GPU texture from CPU-side RGBA8 pixels: `bytes` is a
1067 * bytevector of exactly width*height*4 bytes, row-major with the top
1068 * row first (the same orientation load-texture gives a decoded image).
1069 * `filter` is 'linear (default, as load-texture) or 'nearest.
1071 * The image is created with dynamic_update usage so update-texture can
1072 * refill it; the initial pixels go up through sg_update_image, which
1073 * counts as this frame's one allowed upload (see update-texture).
1074 */
1075static Value native_make_texture_from_pixels(SigilVM *vm, int argc, Value *args)
1077 if (argc < 3) {
1078 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1079 "make-texture-from-pixels: requires width, height, bytes");
1080 return SIGIL_UNDEFINED;
1082 if (!sigil_is_fixnum(args[0]) || !sigil_is_fixnum(args[1])) {
1083 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1084 "make-texture-from-pixels: width and height must be integers");
1085 return SIGIL_UNDEFINED;
1087 int width = (int)sigil_as_fixnum(args[0]);
1088 int height = (int)sigil_as_fixnum(args[1]);
1089 if (width <= 0 || height <= 0) {
1090 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1091 "make-texture-from-pixels: width and height must be positive");
1092 return SIGIL_UNDEFINED;
1094 const uint8_t *pixels = pixels_from_bytevector(vm, args[2], width, height,
1095 "make-texture-from-pixels");
1096 if (!pixels) return SIGIL_UNDEFINED;
1098 sg_filter filter = SG_FILTER_LINEAR;
1099 if (argc > 3 && !filter_from_value(vm, args[3], "make-texture-from-pixels", &filter)) {
1100 return SIGIL_UNDEFINED;
1103 /* dynamic_update images must be created without initial data
1104 * (sokol validates that); the first fill is an update below. */
1105 sg_image_desc img_desc = {
1106 .usage = { .dynamic_update = true },
1107 .width = width,
1108 .height = height,
1109 .pixel_format = SG_PIXELFORMAT_RGBA8,
1110 };
1111 sg_image img = sg_make_image(&img_desc);
1112 if (img.id == SG_INVALID_ID || sg_query_image_state(img) != SG_RESOURCESTATE_VALID) {
1113 if (img.id != SG_INVALID_ID) sg_destroy_image(img); /* a FAILED slot still holds an id */
1114 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1115 "make-texture-from-pixels: failed to create GPU texture");
1116 return SIGIL_FALSE;
1118 sg_image_data data = {
1119 .mip_levels[0] = { .ptr = pixels, .size = (size_t)width * height * 4 }
1120 };
1121 sg_update_image(img, &data);
1123 sg_sampler_desc smp_desc = {
1124 .min_filter = filter,
1125 .mag_filter = filter,
1126 .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
1127 .wrap_v = SG_WRAP_CLAMP_TO_EDGE
1128 };
1129 sg_sampler smp = sg_make_sampler(&smp_desc);
1131 sg_view view = sgp_make_texture_view_from_image(img, "sigil-pixel-texture");
1132 if (view.id == SG_INVALID_ID || sg_query_view_state(view) != SG_RESOURCESTATE_VALID) {
1133 if (view.id != SG_INVALID_ID) sg_destroy_view(view);
1134 sg_destroy_image(img);
1135 sg_destroy_sampler(smp);
1136 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1137 "make-texture-from-pixels: failed to create texture view");
1138 return SIGIL_FALSE;
1141 GfxTexture *tex = malloc(sizeof(GfxTexture));
1142 if (!tex) {
1143 sg_destroy_view(view);
1144 sg_destroy_image(img);
1145 sg_destroy_sampler(smp);
1146 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "make-texture-from-pixels: out of memory");
1147 return SIGIL_FALSE;
1150 tex->handle = img;
1151 tex->sampler = smp;
1152 tex->view = view;
1153 tex->width = width;
1154 tex->height = height;
1155 tex->owns_resources = true;
1156 tex->dynamic = true;
1157 tex->upload_frame = gfx_frame_serial;
1159 ensure_texture_type(vm);
1160 return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
1161 sizeof(GfxTexture));
1165 * (update-texture tex bytes) - Replace a pixel texture's contents
1167 * `tex` must come from make-texture-from-pixels (load-texture images
1168 * are immutable on the GPU); `bytes` has the same shape as at creation:
1169 * width*height*4 RGBA8 bytes, top row first. Errors on a size mismatch.
1171 * sokol_gfx permits one sg_update_image per image per frame (a frame
1172 * ends at end-frame). make-texture-from-pixels spends that frame's
1173 * upload, so the first update-texture of a texture made in the same
1174 * frame is refused; a second update-texture in one frame is refused
1175 * the same way. The error names the texture's last upload frame.
1176 */
1177static Value native_update_texture(SigilVM *vm, int argc, Value *args)
1179 if (argc < 2) {
1180 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1181 "update-texture: requires texture, bytes");
1182 return SIGIL_UNDEFINED;
1184 GfxTexture *tex = get_texture(vm, args[0]);
1185 if (!tex) {
1186 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "update-texture: expected texture");
1187 return SIGIL_UNDEFINED;
1189 if (!tex->dynamic) {
1190 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1191 "update-texture: texture is immutable (only make-texture-from-pixels textures can be updated)");
1192 return SIGIL_UNDEFINED;
1194 const uint8_t *pixels = pixels_from_bytevector(vm, args[1], tex->width, tex->height,
1195 "update-texture");
1196 if (!pixels) return SIGIL_UNDEFINED;
1197 if (tex->upload_frame == gfx_frame_serial) {
1198 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1199 "update-texture: texture already uploaded this frame (sokol_gfx allows one upload per texture per frame; end-frame starts the next)");
1200 return SIGIL_UNDEFINED;
1202 sg_image_data data = {
1203 .mip_levels[0] = { .ptr = pixels, .size = (size_t)tex->width * tex->height * 4 }
1204 };
1205 sg_update_image(tex->handle, &data);
1206 tex->upload_frame = gfx_frame_serial;
1207 return SIGIL_NIL;
1211 * (texture? obj) -> boolean
1212 */
1213static Value native_texture_p(SigilVM *vm, int argc, Value *args)
1215 (void)argc;
1216 return get_texture(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
1220 * (texture-width tex) -> integer
1221 */
1222static Value native_texture_width(SigilVM *vm, int argc, Value *args)
1224 (void)argc;
1225 GfxTexture *tex = get_texture(vm, args[0]);
1226 if (!tex) {
1227 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "texture-width: expected texture");
1228 return SIGIL_UNDEFINED;
1230 return sigil_fixnum(tex->width);
1234 * (texture-height tex) -> integer
1235 */
1236static Value native_texture_height(SigilVM *vm, int argc, Value *args)
1238 (void)argc;
1239 GfxTexture *tex = get_texture(vm, args[0]);
1240 if (!tex) {
1241 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "texture-height: expected texture");
1242 return SIGIL_UNDEFINED;
1244 return sigil_fixnum(tex->height);
1248 * (draw-texture tex x y [w h]) - Draw texture at position
1250 * If w/h are not provided, uses texture's native size.
1251 */
1252static Value native_draw_texture(SigilVM *vm, int argc, Value *args)
1254 if (argc < 3) {
1255 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-texture: requires texture, x, y arguments");
1256 return SIGIL_UNDEFINED;
1259 GfxTexture *tex = get_texture(vm, args[0]);
1260 if (!tex) {
1261 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-texture: expected texture");
1262 return SIGIL_UNDEFINED;
1265 float x = value_to_float(args[1]);
1266 float y = value_to_float(args[2]);
1267 float w = argc > 3 ? value_to_float(args[3]) : (float)tex->width;
1268 float h = argc > 4 ? value_to_float(args[4]) : (float)tex->height;
1270 /* Bind texture view and sampler */
1271 sgp_set_view(0, tex->view);
1272 sgp_set_sampler(0, tex->sampler);
1274 /* Draw textured rectangle - source is entire texture */
1275 sgp_rect dest = {x, y, w, h};
1276 sgp_rect src = {0, 0, (float)tex->width, (float)tex->height};
1277 sgp_draw_textured_rect(0, dest, src);
1279 /* Reset to default (white texture) */
1280 sgp_reset_view(0);
1281 sgp_reset_sampler(0);
1283 return SIGIL_NIL;
1287 * (draw-texture-region tex x y w h sx sy sw sh) - Draw portion of texture
1289 * Draws source region (sx, sy, sw, sh) from texture to destination (x, y, w, h).
1290 */
1291static Value native_draw_texture_region(SigilVM *vm, int argc, Value *args)
1293 if (argc < 9) {
1294 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1295 "draw-texture-region: requires texture, x, y, w, h, sx, sy, sw, sh");
1296 return SIGIL_UNDEFINED;
1299 GfxTexture *tex = get_texture(vm, args[0]);
1300 if (!tex) {
1301 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-texture-region: expected texture");
1302 return SIGIL_UNDEFINED;
1305 float x = value_to_float(args[1]);
1306 float y = value_to_float(args[2]);
1307 float w = value_to_float(args[3]);
1308 float h = value_to_float(args[4]);
1309 float sx = value_to_float(args[5]);
1310 float sy = value_to_float(args[6]);
1311 float sw = value_to_float(args[7]);
1312 float sh = value_to_float(args[8]);
1314 /* Bind texture view and sampler */
1315 sgp_set_view(0, tex->view);
1316 sgp_set_sampler(0, tex->sampler);
1318 /* Draw textured rectangle with source region */
1319 sgp_rect dest = {x, y, w, h};
1320 sgp_rect src = {sx, sy, sw, sh};
1321 sgp_draw_textured_rect(0, dest, src);
1323 /* Reset to default */
1324 sgp_reset_view(0);
1325 sgp_reset_sampler(0);
1327 return SIGIL_NIL;
1330/* ============================================================
1331 * RENDER TARGETS
1332 * ============================================================ */
1334static void ensure_rt_type(SigilVM *vm)
1336 if (sigil_is_undefined(rt_type_tag)) {
1337 rt_type_tag = sigil_intern_symbol(vm, "sigil-graphics-rt", 17);
1341static GfxRenderTarget *get_rt(SigilVM *vm, Value v)
1343 if (!sigil_is_foreign(v)) return NULL;
1344 ensure_rt_type(vm);
1345 if (sigil_foreign_type(v) != rt_type_tag) return NULL;
1346 return (GfxRenderTarget *)sigil_foreign_data(v);
1349/* Free a render target's GPU resources. Idempotent. */
1350static void rt_free_resources(GfxRenderTarget *rt)
1352 if (!rt || rt->freed) return;
1353 /* After gfx-shutdown (sg_shutdown) the handles are already gone and
1354 * sokol asserts on any destroy; a finalizer running then has nothing
1355 * to release. */
1356 if (!sg_isvalid()) { rt->freed = true; return; }
1357 if (rt->color_att_view.id != SG_INVALID_ID) {
1358 sg_destroy_view(rt->color_att_view);
1360 if (rt->tex_view.id != SG_INVALID_ID) {
1361 sg_destroy_view(rt->tex_view);
1363 if (rt->depth_att_view.id != SG_INVALID_ID) {
1364 sg_destroy_view(rt->depth_att_view);
1366 if (rt->color_img.id != SG_INVALID_ID) {
1367 sg_destroy_image(rt->color_img);
1369 if (rt->depth_img.id != SG_INVALID_ID) {
1370 sg_destroy_image(rt->depth_img);
1372 if (rt->sampler.id != SG_INVALID_ID) {
1373 sg_destroy_sampler(rt->sampler);
1375 rt->freed = true;
1378static void rt_destructor(void *data)
1380 GfxRenderTarget *rt = (GfxRenderTarget *)data;
1381 if (rt) {
1382 rt_free_resources(rt);
1383 free(rt);
1388 * (%make-render-target width height) -> <render-target> or #f
1390 * Creates an offscreen color render target backed by an sg_image with
1391 * color_attachment usage, plus the views and sampler needed to render
1392 * into it and sample it as a texture.
1393 */
1394static Value native_make_render_target(SigilVM *vm, int argc, Value *args)
1396 if (argc < 2) {
1397 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1398 "make-render-target: requires width, height arguments");
1399 return SIGIL_UNDEFINED;
1402 int w = (int)value_to_float(args[0]);
1403 int h = (int)value_to_float(args[1]);
1404 if (w <= 0 || h <= 0) {
1405 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1406 "make-render-target: width and height must be positive");
1407 return SIGIL_FALSE;
1410 /* Optional third argument: the sampler filter used when this target
1411 * is drawn as a texture — 'linear (default, the v0.10 behaviour) or
1412 * 'nearest for texel-exact reads (cell grids, pixel art). Note that
1413 * GLSL texelFetch bypasses the sampler entirely, so a shader that
1414 * reads neighbours by integer texel works with either. */
1415 sg_filter filter = SG_FILTER_LINEAR;
1416 if (argc > 2 && !filter_from_value(vm, args[2], "make-render-target", &filter)) {
1417 return SIGIL_UNDEFINED;
1420 /* Pixel format and sample count default to sg_environment.defaults
1421 * (i.e., the swap chain's color format / sample count). Matching them
1422 * is required so sgp's default pipelines validate against this pass —
1423 * sgp pipelines bake in the format/sample count from sgp_setup. */
1424 sg_image_desc img_desc = {
1425 .usage = { .color_attachment = true },
1426 .width = w,
1427 .height = h,
1428 };
1429 sg_image img = sg_make_image(&img_desc);
1430 if (img.id == SG_INVALID_ID || sg_query_image_state(img) != SG_RESOURCESTATE_VALID) {
1431 if (img.id != SG_INVALID_ID) sg_destroy_image(img); /* a FAILED slot still holds an id */
1432 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1433 "make-render-target: failed to create color image");
1434 return SIGIL_FALSE;
1437 sg_view_desc att_desc = {
1438 .color_attachment = { .image = img },
1439 .label = "sigil-rt-color-attachment",
1440 };
1441 sg_view att_view = sg_make_view(&att_desc);
1442 if (att_view.id == SG_INVALID_ID || sg_query_view_state(att_view) != SG_RESOURCESTATE_VALID) {
1443 if (att_view.id != SG_INVALID_ID) sg_destroy_view(att_view);
1444 sg_destroy_image(img);
1445 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1446 "make-render-target: failed to create color attachment view");
1447 return SIGIL_FALSE;
1450 sg_view tex_view = sgp_make_texture_view_from_image(img, "sigil-rt-texture");
1451 if (tex_view.id == SG_INVALID_ID || sg_query_view_state(tex_view) != SG_RESOURCESTATE_VALID) {
1452 if (tex_view.id != SG_INVALID_ID) sg_destroy_view(tex_view);
1453 sg_destroy_view(att_view);
1454 sg_destroy_image(img);
1455 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1456 "make-render-target: failed to create texture view");
1457 return SIGIL_FALSE;
1460 /* Depth-stencil attachment — needed only for pipeline-validation
1461 * compatibility with sgp's default pipelines, which bake in the
1462 * environment's default depth format. Natively that is the swap
1463 * chain's depth-stencil format, so the pass must carry a matching
1464 * attachment. On the web build sig_gfx_environment() declares
1465 * depth_format = SG_PIXELFORMAT_NONE: the pipelines expect NO depth
1466 * attachment, and an image made with that format fails
1467 * (GL_TEXTURE_FORMAT_NOT_SUPPORTED), which left every web render
1468 * target's pass refused with BEGINPASS_ATTACHMENTS_ALIVE before
1469 * v0.11.3. So: only when the environment has a depth format. */
1470 sg_image depth_img = { SG_INVALID_ID };
1471 sg_view depth_att_view = { SG_INVALID_ID };
1472 bool want_depth = sg_query_desc().environment.defaults.depth_format != SG_PIXELFORMAT_NONE;
1473 if (want_depth) {
1474 sg_image_desc depth_desc = {
1475 .usage = { .depth_stencil_attachment = true },
1476 .width = w,
1477 .height = h,
1478 };
1479 depth_img = sg_make_image(&depth_desc);
1480 if (depth_img.id == SG_INVALID_ID || sg_query_image_state(depth_img) != SG_RESOURCESTATE_VALID) {
1481 if (depth_img.id != SG_INVALID_ID) sg_destroy_image(depth_img);
1482 sg_destroy_view(tex_view);
1483 sg_destroy_view(att_view);
1484 sg_destroy_image(img);
1485 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1486 "make-render-target: failed to create depth image");
1487 return SIGIL_FALSE;
1490 sg_view_desc depth_att_desc = {
1491 .depth_stencil_attachment = { .image = depth_img },
1492 .label = "sigil-rt-depth-attachment",
1493 };
1494 depth_att_view = sg_make_view(&depth_att_desc);
1495 if (depth_att_view.id == SG_INVALID_ID || sg_query_view_state(depth_att_view) != SG_RESOURCESTATE_VALID) {
1496 if (depth_att_view.id != SG_INVALID_ID) sg_destroy_view(depth_att_view);
1497 sg_destroy_image(depth_img);
1498 sg_destroy_view(tex_view);
1499 sg_destroy_view(att_view);
1500 sg_destroy_image(img);
1501 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1502 "make-render-target: failed to create depth attachment view");
1503 return SIGIL_FALSE;
1507 sg_sampler_desc smp_desc = {
1508 .min_filter = filter,
1509 .mag_filter = filter,
1510 .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
1511 .wrap_v = SG_WRAP_CLAMP_TO_EDGE,
1512 };
1513 sg_sampler smp = sg_make_sampler(&smp_desc);
1515 GfxRenderTarget *rt = malloc(sizeof(GfxRenderTarget));
1516 if (!rt) {
1517 sg_destroy_sampler(smp);
1518 if (depth_att_view.id != SG_INVALID_ID) sg_destroy_view(depth_att_view);
1519 if (depth_img.id != SG_INVALID_ID) sg_destroy_image(depth_img);
1520 sg_destroy_view(tex_view);
1521 sg_destroy_view(att_view);
1522 sg_destroy_image(img);
1523 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
1524 "make-render-target: out of memory");
1525 return SIGIL_FALSE;
1528 rt->color_img = img;
1529 rt->color_att_view = att_view;
1530 rt->tex_view = tex_view;
1531 rt->sampler = smp;
1532 rt->depth_img = depth_img;
1533 rt->depth_att_view = depth_att_view;
1534 rt->width = w;
1535 rt->height = h;
1536 rt->freed = false;
1538 ensure_rt_type(vm);
1539 return sigil_make_foreign(vm, rt_type_tag, rt, rt_destructor,
1540 sizeof(GfxRenderTarget));
1544 * (render-target? obj) -> boolean
1545 */
1546static Value native_render_target_p(SigilVM *vm, int argc, Value *args)
1548 (void)argc;
1549 return get_rt(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
1553 * (render-target-width rt) -> integer
1554 */
1555static Value native_render_target_width(SigilVM *vm, int argc, Value *args)
1557 (void)argc;
1558 GfxRenderTarget *rt = get_rt(vm, args[0]);
1559 if (!rt) {
1560 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1561 "render-target-width: expected render-target");
1562 return SIGIL_UNDEFINED;
1564 return sigil_fixnum(rt->width);
1568 * (render-target-height rt) -> integer
1569 */
1570static Value native_render_target_height(SigilVM *vm, int argc, Value *args)
1572 (void)argc;
1573 GfxRenderTarget *rt = get_rt(vm, args[0]);
1574 if (!rt) {
1575 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1576 "render-target-height: expected render-target");
1577 return SIGIL_UNDEFINED;
1579 return sigil_fixnum(rt->height);
1583 * (render-target-free! rt) - Explicit cleanup of GPU resources
1585 * Idempotent. After this call the render target is unusable; the
1586 * destructor on GC will be a no-op.
1587 */
1588static Value native_render_target_free(SigilVM *vm, int argc, Value *args)
1590 (void)argc;
1591 GfxRenderTarget *rt = get_rt(vm, args[0]);
1592 if (!rt) {
1593 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1594 "render-target-free!: expected render-target");
1595 return SIGIL_UNDEFINED;
1597 rt_free_resources(rt);
1598 return SIGIL_NIL;
1602 * (%begin-rt-pass rt) - Push a new sokol_gp queue + sokol_gfx pass that
1603 * targets the render target. All subsequent draws land in rt's color
1604 * image until %end-rt-pass is called.
1605 */
1606static Value native_begin_rt_pass(SigilVM *vm, int argc, Value *args)
1608 (void)argc;
1609 GfxRenderTarget *rt = get_rt(vm, args[0]);
1610 if (!rt) {
1611 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1612 "%begin-rt-pass: expected render-target");
1613 return SIGIL_UNDEFINED;
1615 if (rt->freed) {
1616 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1617 "%begin-rt-pass: render target has been freed");
1618 return SIGIL_UNDEFINED;
1621 /* Push an inner sgp state on the stack with the render target's size.
1622 * sgp's state stack ensures inner sgp_flush only emits commands queued
1623 * between this sgp_begin and its matching sgp_end. */
1624 sgp_begin(rt->width, rt->height);
1625 sgp_viewport(0, 0, rt->width, rt->height);
1626 sgp_project(0, (float)rt->width, 0, (float)rt->height);
1628 /* Color: clear to transparent black on entry, keep contents on exit
1629 * (the swap-chain composite reads it). Depth: don't load, don't
1630 * store — the depth attachment exists only to satisfy sgp's
1631 * pipeline-validation requirement that pass and pipeline depth
1632 * formats match; 2D rendering never reads or writes it. */
1633 sg_pass_action act = {
1634 .colors[0] = {
1635 .load_action = SG_LOADACTION_CLEAR,
1636 .store_action = SG_STOREACTION_STORE,
1637 .clear_value = {0.0f, 0.0f, 0.0f, 0.0f},
1638 },
1639 .depth = {
1640 .load_action = SG_LOADACTION_DONTCARE,
1641 .store_action = SG_STOREACTION_DONTCARE,
1642 },
1643 };
1644 sg_pass pass = {
1645 .action = act,
1646 .attachments = {
1647 .colors[0] = rt->color_att_view,
1648 .depth_stencil = rt->depth_att_view,
1649 },
1650 };
1651 sg_begin_pass(&pass);
1653 /* A shader bound inside this pass sees u_resolution = the target's
1654 * size (it is the surface being drawn), not the window's. */
1655 rt_pass_active = true;
1656 rt_pass_width = rt->width;
1657 rt_pass_height = rt->height;
1659 return SIGIL_NIL;
1663 * (%end-rt-pass) - Flush queued commands to the current render target,
1664 * end the sokol_gfx pass, and pop the inner sgp state.
1666 * Must balance a prior %begin-rt-pass call.
1667 */
1668static Value native_end_rt_pass(SigilVM *vm, int argc, Value *args)
1670 (void)vm; (void)argc; (void)args;
1672 sgp_flush();
1673 sg_end_pass();
1674 sgp_end();
1675 rt_pass_active = false;
1677 return SIGIL_NIL;
1681 * (render-target->texture rt) -> <texture>
1683 * Returns a texture wrapper that borrows the render target's image,
1684 * texture view, and sampler. The returned texture is invalid after the
1685 * render target is freed; callers must keep the render target alive for
1686 * the lifetime of the wrapper.
1687 */
1688static Value native_render_target_to_texture(SigilVM *vm, int argc, Value *args)
1690 (void)argc;
1691 GfxRenderTarget *rt = get_rt(vm, args[0]);
1692 if (!rt) {
1693 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1694 "render-target->texture: expected render-target");
1695 return SIGIL_UNDEFINED;
1698 GfxTexture *tex = malloc(sizeof(GfxTexture));
1699 if (!tex) {
1700 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
1701 "render-target->texture: out of memory");
1702 return SIGIL_FALSE;
1705 tex->handle = rt->color_img;
1706 tex->sampler = rt->sampler;
1707 tex->view = rt->tex_view;
1708 tex->width = rt->width;
1709 tex->height = rt->height;
1710 tex->owns_resources = false;
1711 tex->dynamic = false;
1712 tex->upload_frame = 0;
1714 ensure_texture_type(vm);
1715 return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
1716 sizeof(GfxTexture));
1720 * (draw-render-target rt x y w h) - Draw the render target's color image
1721 * as a textured rect on the current pass.
1723 * Convenience over (draw-texture (render-target->texture rt) x y w h):
1724 * skips the texture wrapper allocation.
1725 */
1726static Value native_draw_render_target(SigilVM *vm, int argc, Value *args)
1728 if (argc < 5) {
1729 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1730 "draw-render-target: requires rt, x, y, w, h");
1731 return SIGIL_UNDEFINED;
1733 GfxRenderTarget *rt = get_rt(vm, args[0]);
1734 if (!rt) {
1735 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1736 "draw-render-target: expected render-target");
1737 return SIGIL_UNDEFINED;
1740 float x = value_to_float(args[1]);
1741 float y = value_to_float(args[2]);
1742 float w = value_to_float(args[3]);
1743 float h = value_to_float(args[4]);
1745 sgp_set_view(0, rt->tex_view);
1746 sgp_set_sampler(0, rt->sampler);
1748 /* Flip V on sample: sokol_gfx framebuffers write to texture memory in
1749 * GL bottom-up convention even on top-down backends, so a render
1750 * target sampled with the default top-down UV looks vertically
1751 * mirrored on the swap chain. Sourcing from y=height with h=-height
1752 * inverts the texcoord range so the composite is upright. */
1753 sgp_rect dest = {x, y, w, h};
1754 sgp_rect src = {0, (float)rt->height, (float)rt->width, -(float)rt->height};
1755 sgp_draw_textured_rect(0, dest, src);
1757 sgp_reset_view(0);
1758 sgp_reset_sampler(0);
1760 return SIGIL_NIL;
1763/* ============================================================
1764 * SHADERS (Phase 2 — custom fragment shaders for post-processing)
1765 * ============================================================ */
1767static void ensure_shader_type(SigilVM *vm)
1769 if (sigil_is_undefined(shader_type_tag)) {
1770 shader_type_tag = sigil_intern_symbol(vm, "sigil-graphics-shader", 21);
1774static GfxShader *get_shader(SigilVM *vm, Value v)
1776 if (!sigil_is_foreign(v)) return NULL;
1777 ensure_shader_type(vm);
1778 if (sigil_foreign_type(v) != shader_type_tag) return NULL;
1779 return (GfxShader *)sigil_foreign_data(v);
1782static void shader_free_resources(GfxShader *sh)
1784 if (!sh || sh->freed) return;
1785 if (!sg_isvalid()) { sh->freed = true; active_shader = (active_shader == sh) ? NULL : active_shader; return; }
1786 if (active_shader == sh) {
1787 active_shader = NULL;
1789 if (sh->pipeline.id != SG_INVALID_ID) {
1790 sg_destroy_pipeline(sh->pipeline);
1792 if (sh->shader.id != SG_INVALID_ID) {
1793 sg_destroy_shader(sh->shader);
1795 sh->freed = true;
1798static void shader_destructor(void *data)
1800 GfxShader *sh = (GfxShader *)data;
1801 if (sh) {
1802 shader_free_resources(sh);
1803 free(sh);
1807/* Map a GLSL type token to sokol_gfx uniform type + size in bytes
1808 * (NATIVE layout — same as STD140 except for vec3, which we don't use).
1809 * Returns 0 on unrecognized type. */
1810static int map_glsl_type(const char *tok, sg_uniform_type *out_type, uint32_t *out_size)
1812 if (strcmp(tok, "float") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT; *out_size = 4; return 1; }
1813 if (strcmp(tok, "vec2") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT2; *out_size = 8; return 1; }
1814 if (strcmp(tok, "vec3") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT3; *out_size = 12; return 1; }
1815 if (strcmp(tok, "vec4") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT4; *out_size = 16; return 1; }
1816 if (strcmp(tok, "mat4") == 0) { *out_type = SG_UNIFORMTYPE_MAT4; *out_size = 64; return 1; }
1817 return 0;
1820static int is_glsl_space(char c)
1822 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
1825/* Parse the user's fragment GLSL for `uniform <type> <name>;` and
1826 * `uniform <type> <name>[N];` declarations.
1828 * Builds the GfxShader's uniform layout. Skips sampler2D — those are
1829 * texture bindings, not uniform-block entries (the channel-0 binding is
1830 * managed by the draw primitive). Recognised types: float, vec2, vec3,
1831 * vec4, mat4, optionally preceded by a precision qualifier (lowp /
1832 * mediump / highp, as GLSL ES sources write). Unknown types are silently
1833 * skipped (the shader-create call will fail later at link time if names
1834 * mismatch — fine).
1836 * An array suffix `[N]` (N a literal positive integer; whitespace is
1837 * allowed around N and before the bracket) makes the entry N elements
1838 * long: size = N * element size, array_count = N. A malformed suffix
1839 * (no literal, no closing bracket) skips the declaration the same way an
1840 * unknown type does, so the link reports the mismatch.
1842 * NATIVE layout: tightly packed, no padding — arrays included, which is
1843 * what sokol_gfx's _sg_uniform_size computes for SG_UNIFORMLAYOUT_NATIVE
1844 * and what glUniform*fv consumes. A 17th member (sokol's
1845 * SG_MAX_UNIFORMBLOCK_MEMBERS) or a block past SIGIL_GFX_UNIFORM_BUFFER_SIZE
1846 * is reported to load-shader as an error rather than dropped: sokol only
1847 * warns about names in the desc that the program lacks, never about
1848 * program uniforms the desc omits, so a silently dropped member would
1849 * read as zero with nothing logged. */
1850/* Returns 0, or SIGIL_GFX_PARSE_TOO_MANY / SIGIL_GFX_PARSE_TOO_LARGE when a
1851 * declaration does not fit; the caller (load-shader) reports it. */
1852#define SIGIL_GFX_PARSE_TOO_MANY 1
1853#define SIGIL_GFX_PARSE_TOO_LARGE 2
1854static int parse_fragment_uniforms(GfxShader *sh, const char *frag_src)
1856 const char *p = frag_src;
1857 uint32_t cursor = 0;
1858 sh->num_uniforms = 0;
1859 sh->u_time_index = -1;
1860 sh->u_resolution_index = -1;
1862 while (*p) {
1863 /* Look for the literal "uniform" token at a line boundary or
1864 * after whitespace. */
1865 const char *u = strstr(p, "uniform");
1866 if (!u) break;
1867 if (u != frag_src && !is_glsl_space(u[-1])) {
1868 p = u + 7;
1869 continue;
1871 const char *q = u + 7;
1872 if (!is_glsl_space(*q)) { p = q; continue; }
1873 while (is_glsl_space(*q)) q++;
1874 /* Read type token; a precision qualifier first is skipped. */
1875 char type_buf[32];
1876 size_t ti = 0;
1877 while (*q && !is_glsl_space(*q) && *q != ';' && ti < sizeof(type_buf)-1) {
1878 type_buf[ti++] = *q++;
1880 type_buf[ti] = '\0';
1881 if (strcmp(type_buf, "lowp") == 0 || strcmp(type_buf, "mediump") == 0 ||
1882 strcmp(type_buf, "highp") == 0) {
1883 while (is_glsl_space(*q)) q++;
1884 ti = 0;
1885 while (*q && !is_glsl_space(*q) && *q != ';' && ti < sizeof(type_buf)-1) {
1886 type_buf[ti++] = *q++;
1888 type_buf[ti] = '\0';
1890 /* Skip sampler2D / sampler types — those are texture bindings,
1891 * not uniform-block entries. */
1892 if (strncmp(type_buf, "sampler", 7) == 0) {
1893 p = q;
1894 continue;
1896 sg_uniform_type utype = SG_UNIFORMTYPE_INVALID;
1897 uint32_t elem_size = 0;
1898 if (!map_glsl_type(type_buf, &utype, &elem_size)) {
1899 p = q;
1900 continue;
1902 while (is_glsl_space(*q)) q++;
1903 /* Read name token (until ';', whitespace or '['). */
1904 char name_buf[SIGIL_GFX_UNIFORM_NAME_MAX];
1905 size_t ni = 0;
1906 while (*q && *q != ';' && !is_glsl_space(*q) && *q != '[' &&
1907 ni < sizeof(name_buf)-1) {
1908 name_buf[ni++] = *q++;
1910 name_buf[ni] = '\0';
1911 if (ni == 0) { p = q; continue; }
1912 /* Optional array suffix. */
1913 int array_count = 1;
1914 while (is_glsl_space(*q)) q++;
1915 if (*q == '[') {
1916 q++;
1917 while (is_glsl_space(*q)) q++;
1918 long n = 0;
1919 int digits = 0;
1920 while (*q >= '0' && *q <= '9') {
1921 n = n * 10 + (*q - '0');
1922 digits++;
1923 q++;
1924 if (n > 4096) break; /* absurd; bail on this decl */
1926 while (is_glsl_space(*q)) q++;
1927 if (digits == 0 || n <= 0 || n > 4096 || *q != ']') {
1928 /* Not a literal-sized array we can lay out; skip it. */
1929 p = q;
1930 continue;
1932 q++;
1933 array_count = (int)n;
1935 uint32_t usize = elem_size * (uint32_t)array_count;
1936 if (sh->num_uniforms >= SIGIL_GFX_MAX_UNIFORMS) return SIGIL_GFX_PARSE_TOO_MANY;
1937 if (cursor + usize > sizeof(sh->buffer)) return SIGIL_GFX_PARSE_TOO_LARGE;
1938 GfxUniformEntry *ent = &sh->uniforms[sh->num_uniforms];
1939 strncpy(ent->name, name_buf, sizeof(ent->name)-1);
1940 ent->name[sizeof(ent->name)-1] = '\0';
1941 ent->type = utype;
1942 ent->offset = cursor;
1943 ent->size = usize;
1944 ent->elem_size = elem_size;
1945 ent->array_count = array_count;
1946 if (strcmp(name_buf, "u_time") == 0) {
1947 sh->u_time_index = sh->num_uniforms;
1948 } else if (strcmp(name_buf, "u_resolution") == 0) {
1949 sh->u_resolution_index = sh->num_uniforms;
1951 cursor += usize;
1952 sh->num_uniforms++;
1953 p = q;
1955 sh->buffer_size = cursor;
1956 memset(sh->buffer, 0, sizeof(sh->buffer));
1957 return 0;
1960/* (load-shader vertex-source fragment-source [blend]) -> <shader> or #f
1962 * blend is the pipeline's baked blend mode: 'normal (alpha blend, the
1963 * default and the v0.10 behaviour), 'additive, or 'none (overwrite: what
1964 * a simulation step writing state into a render target needs, since
1965 * alpha-blending data channels against the cleared target corrupts
1966 * them). set-blend-mode does not reach a custom pipeline; this does. */
1967static Value native_load_shader(SigilVM *vm, int argc, Value *args)
1969 if (argc < 2) {
1970 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1971 "load-shader: requires vertex-source, fragment-source [blend]");
1972 return SIGIL_UNDEFINED;
1975 const char *vs_src = sigil_string_bytes(args[0]);
1976 const char *fs_src = sigil_string_bytes(args[1]);
1977 if (!vs_src || !fs_src) {
1978 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1979 "load-shader: expected two strings");
1980 return SIGIL_FALSE;
1983 int blend = SGP_BLENDMODE_BLEND;
1984 if (argc > 2) {
1985 blend = blend_mode_from_value(args[2]);
1986 if (blend < 0) {
1987 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1988 "load-shader: blend must be 'normal, 'additive or 'none");
1989 return SIGIL_UNDEFINED;
1993 GfxShader *sh = malloc(sizeof(GfxShader));
1994 if (!sh) {
1995 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
1996 "load-shader: out of memory");
1997 return SIGIL_FALSE;
1999 memset(sh, 0, sizeof(*sh));
2000 sh->shader.id = SG_INVALID_ID;
2001 sh->pipeline.id = SG_INVALID_ID;
2003 int parse_rc = parse_fragment_uniforms(sh, fs_src);
2004 if (parse_rc != 0) {
2005 char msg[200];
2006 snprintf(msg, sizeof(msg), parse_rc == SIGIL_GFX_PARSE_TOO_MANY
2007 ? "load-shader: more than %d uniform declarations (sokol_gfx's per-block cap; an array counts once)"
2008 : "load-shader: uniform declarations exceed the %d-byte block (SIGIL_GFX_UNIFORM_BUFFER_SIZE)",
2009 parse_rc == SIGIL_GFX_PARSE_TOO_MANY ? SIGIL_GFX_MAX_UNIFORMS : SIGIL_GFX_UNIFORM_BUFFER_SIZE);
2010 free(sh);
2011 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, msg);
2012 return SIGIL_UNDEFINED;
2015 sg_shader_desc desc = {0};
2016 /* Vertex attributes — must match sgp's vertex layout (location 0 =
2017 * vec4 coord, location 1 = vec4 color). The shader the user writes
2018 * must declare these inputs at the matching locations; for GL the
2019 * link uses glBindAttribLocation by glsl_name. */
2020 desc.attrs[SGP_VS_ATTR_COORD].glsl_name = "coord";
2021 desc.attrs[SGP_VS_ATTR_COLOR].glsl_name = "color";
2022 /* One sampler+view pair on the fragment stage at slot 0 — matches
2023 * sgp's default convention. The user fragment shader names this
2024 * sampler "iTexChannel0_iSmpChannel0" (sgp's canonical name). */
2025 desc.samplers[0].stage = SG_SHADERSTAGE_FRAGMENT;
2026 desc.samplers[0].sampler_type = SG_SAMPLERTYPE_FILTERING;
2027 desc.views[0].texture.stage = SG_SHADERSTAGE_FRAGMENT;
2028 desc.views[0].texture.image_type = SG_IMAGETYPE_2D;
2029 desc.views[0].texture.sample_type = SG_IMAGESAMPLETYPE_FLOAT;
2030 desc.texture_sampler_pairs[0].stage = SG_SHADERSTAGE_FRAGMENT;
2031 desc.texture_sampler_pairs[0].view_slot = 0;
2032 desc.texture_sampler_pairs[0].sampler_slot = 0;
2033 desc.texture_sampler_pairs[0].glsl_name = "iTexChannel0_iSmpChannel0";
2035 /* Uniform block on the fragment stage at slot 1 (matches sgp's
2036 * SGP_UNIFORM_SLOT_FRAGMENT — sgp_flush emits fragment uniforms
2037 * to slot 1 when sgp_set_uniform's fs_size > 0). NATIVE layout —
2038 * tightly packed, alignment=1, matches the parser's offset
2039 * computation. array_count is 1 for scalars/vectors and N for a
2040 * `name[N]` declaration (sokol asserts > 0 in _sg_uniform_size and
2041 * passes it to glUniform*fv as the element count). */
2042 if (sh->num_uniforms > 0) {
2043 desc.uniform_blocks[1].stage = SG_SHADERSTAGE_FRAGMENT;
2044 desc.uniform_blocks[1].size = sh->buffer_size;
2045 desc.uniform_blocks[1].layout = SG_UNIFORMLAYOUT_NATIVE;
2046 for (int i = 0; i < sh->num_uniforms; i++) {
2047 desc.uniform_blocks[1].glsl_uniforms[i].type = sh->uniforms[i].type;
2048 desc.uniform_blocks[1].glsl_uniforms[i].glsl_name = sh->uniforms[i].name;
2049 desc.uniform_blocks[1].glsl_uniforms[i].array_count = sh->uniforms[i].array_count;
2053 desc.vertex_func.entry = "main";
2054 desc.fragment_func.entry = "main";
2055 desc.vertex_func.source = vs_src;
2056 desc.fragment_func.source = fs_src;
2058 sh->shader = sg_make_shader(&desc);
2059 if (sh->shader.id == SG_INVALID_ID || sg_query_shader_state(sh->shader) != SG_RESOURCESTATE_VALID) {
2060 /* a compile/link failure leaves a FAILED slot with a real id */
2061 if (sh->shader.id != SG_INVALID_ID) sg_destroy_shader(sh->shader);
2062 free(sh);
2063 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
2064 "load-shader: shader compile/link failed (see sokol log above)");
2065 return SIGIL_FALSE;
2068 /* Build a custom sgp pipeline for this shader. Default to alpha blend
2069 * since post-processing typically overlays a textured rect on the
2070 * swap chain. */
2071 sgp_pipeline_desc pip_desc = {0};
2072 pip_desc.shader = sh->shader;
2073 pip_desc.blend_mode = (sgp_blend_mode)blend;
2074 pip_desc.has_vs_color = true; /* sgp's vertex layout always has color */
2075 sh->pipeline = sgp_make_pipeline(&pip_desc);
2076 if (sh->pipeline.id == SG_INVALID_ID || sg_query_pipeline_state(sh->pipeline) != SG_RESOURCESTATE_VALID) {
2077 if (sh->pipeline.id != SG_INVALID_ID) sg_destroy_pipeline(sh->pipeline);
2078 sg_destroy_shader(sh->shader);
2079 free(sh);
2080 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
2081 "load-shader: pipeline create failed");
2082 return SIGIL_FALSE;
2085 ensure_shader_type(vm);
2086 return sigil_make_foreign(vm, shader_type_tag, sh, shader_destructor,
2087 sizeof(GfxShader));
2090/* (shader? obj) -> boolean */
2091static Value native_shader_p(SigilVM *vm, int argc, Value *args)
2093 (void)argc;
2094 return get_shader(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
2097/* (shader-free! shader) — explicit cleanup. Idempotent. */
2098static Value native_shader_free(SigilVM *vm, int argc, Value *args)
2100 (void)argc;
2101 GfxShader *sh = get_shader(vm, args[0]);
2102 if (!sh) {
2103 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2104 "shader-free!: expected shader");
2105 return SIGIL_UNDEFINED;
2107 shader_free_resources(sh);
2108 return SIGIL_NIL;
2111/* Find a uniform by name in the shader's layout. Returns -1 if absent. */
2112static int shader_uniform_index(const GfxShader *sh, const char *name)
2114 for (int i = 0; i < sh->num_uniforms; i++) {
2115 if (strcmp(sh->uniforms[i].name, name) == 0) return i;
2117 return -1;
2120/* Refresh u_time + u_resolution into the active shader's buffer and
2121 * push the buffer to the fragment stage uniform block. Called from
2122 * draw primitives that use the active shader. */
2123static void apply_active_shader_uniforms(void)
2125 GfxShader *sh = active_shader;
2126 if (!sh || sh->freed) return;
2127 if (sh->u_time_index >= 0) {
2128 float t = gfx_elapsed_seconds();
2129 memcpy(sh->buffer + sh->uniforms[sh->u_time_index].offset,
2130 &t, sizeof(t));
2132 if (sh->u_resolution_index >= 0) {
2133 float res[2] = {(float)sig_gfx_width(), (float)sig_gfx_height()};
2134 if (virtual_viewport_enabled) {
2135 res[0] = (float)virtual_width;
2136 res[1] = (float)virtual_height;
2138 if (rt_pass_active) {
2139 /* Inside with-render-target the target is the surface being
2140 * drawn, so its size is the resolution the shader wants
2141 * (texel steps, gl_FragCoord normalisation). */
2142 res[0] = (float)rt_pass_width;
2143 res[1] = (float)rt_pass_height;
2145 memcpy(sh->buffer + sh->uniforms[sh->u_resolution_index].offset,
2146 res, sizeof(res));
2148 if (sh->buffer_size > 0) {
2149 sgp_set_uniform(NULL, 0, sh->buffer, sh->buffer_size);
2153/* Flatten a number / list / vector (nested one level or more) into
2154 * `out`, at most `cap` floats. Returns the count written, or -1 if a
2155 * non-number leaf was met or the count would exceed `cap`. */
2156#define SIGIL_GFX_FLATTEN_MAX_DEPTH 8
2157static int flatten_floats(Value v, float *out, int cap, int n, int depth)
2159 if (depth > SIGIL_GFX_FLATTEN_MAX_DEPTH) return -1; /* a cyclic structure, or absurd nesting */
2160 if (sigil_is_fixnum(v) || sigil_is_flonum(v)) {
2161 if (n >= cap) return -1;
2162 out[n] = value_to_float(v);
2163 return n + 1;
2165 if (sigil_is_pair(v) || sigil_is_null(v)) {
2166 Value cur = v;
2167 while (sigil_is_pair(cur)) {
2168 n = flatten_floats(sigil_car(cur), out, cap, n, depth + 1);
2169 if (n < 0) return -1;
2170 cur = sigil_cdr(cur);
2172 return n;
2174 if (sigil_is_vector(v)) {
2175 size_t len = sigil_vector_length(v);
2176 for (size_t i = 0; i < len; i++) {
2177 n = flatten_floats(sigil_vector_ref(v, i), out, cap, n, depth + 1);
2178 if (n < 0) return -1;
2180 return n;
2182 return -1;
2185/* (set-shader-uniform shader name value) — write a value into the
2186 * shader's uniform buffer at the offset matching `name`.
2188 * Accepts:
2189 * number → float
2190 * list/vector of 2/3/4 → vec2 / vec3 / vec4
2191 * 16 floats → mat4 (pass as a list or vector)
2192 * arrays (`name[N]`) → a list or vector holding N*components
2193 * floats, flat or nested per element, e.g.
2194 * '((1 0 0 1) (0 1 0 1)) for vec4 palette[2]
2196 * The float count must equal the uniform's total size exactly; a
2197 * mismatch is an error, never a partial write.
2199 * sampler2D uniforms are NOT set through this path; channel 0 is bound
2200 * by the draw primitive (e.g., draw-render-target) and additional
2201 * samplers are out of scope. */
2202static Value native_set_shader_uniform(SigilVM *vm, int argc, Value *args)
2204 if (argc < 3) {
2205 sigil__vm_error(vm, SIGIL_ERR_ARITY,
2206 "set-shader-uniform: requires shader, name, value");
2207 return SIGIL_UNDEFINED;
2209 GfxShader *sh = get_shader(vm, args[0]);
2210 if (!sh) {
2211 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2212 "set-shader-uniform: expected shader");
2213 return SIGIL_UNDEFINED;
2215 const char *name = NULL;
2216 if (sigil_is_symbol(args[1])) {
2217 name = sigil_symbol_name(args[1]);
2218 } else if (sigil_is_string(args[1])) {
2219 name = sigil_string_bytes(args[1]);
2221 if (!name) {
2222 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2223 "set-shader-uniform: name must be symbol or string");
2224 return SIGIL_UNDEFINED;
2226 int idx = shader_uniform_index(sh, name);
2227 if (idx < 0) {
2228 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
2229 "set-shader-uniform: no such uniform in shader");
2230 return SIGIL_UNDEFINED;
2232 GfxUniformEntry *ent = &sh->uniforms[idx];
2233 Value v = args[2];
2234 uint32_t needed = ent->size / sizeof(float);
2235 /* Number → float (a bare number is only valid for a scalar float). */
2236 if (sigil_is_fixnum(v) || sigil_is_flonum(v)) {
2237 if (ent->type != SG_UNIFORMTYPE_FLOAT || ent->array_count != 1) {
2238 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2239 "set-shader-uniform: type mismatch (got number, uniform is not a single float)");
2240 return SIGIL_UNDEFINED;
2242 float f = value_to_float(v);
2243 memcpy(sh->buffer + ent->offset, &f, sizeof(f));
2244 } else if (sigil_is_pair(v) || sigil_is_null(v) || sigil_is_vector(v)) {
2245 /* List or vector of numbers, flat or nested per element. */
2246 float scratch[SIGIL_GFX_UNIFORM_BUFFER_SIZE / sizeof(float)];
2247 int n = flatten_floats(v, scratch, (int)(sizeof(scratch) / sizeof(float)), 0, 0);
2248 if (n < 0) {
2249 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2250 "set-shader-uniform: value must contain only numbers (nested lists/vectors allowed)");
2251 return SIGIL_UNDEFINED;
2253 if ((uint32_t)n != needed) {
2254 char msg[200];
2255 snprintf(msg, sizeof(msg),
2256 "set-shader-uniform: %s needs %u floats, got %d",
2257 ent->name, (unsigned)needed, n);
2258 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, msg);
2259 return SIGIL_UNDEFINED;
2261 memcpy(sh->buffer + ent->offset, scratch, ent->size);
2262 } else {
2263 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2264 "set-shader-uniform: value must be a number, list or vector of numbers");
2265 return SIGIL_UNDEFINED;
2267 /* If this shader is currently active, push the updated buffer right
2268 * away so the new value lands in the next draw without waiting for
2269 * the next auto-refresh. */
2270 if (active_shader == sh && sh->buffer_size > 0) {
2271 sgp_set_uniform(NULL, 0, sh->buffer, sh->buffer_size);
2273 return SIGIL_NIL;
2276/* (%bind-shader shader) — set as active sgp pipeline + uniform source. */
2277static Value native_bind_shader(SigilVM *vm, int argc, Value *args)
2279 (void)argc;
2280 GfxShader *sh = get_shader(vm, args[0]);
2281 if (!sh) {
2282 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
2283 "%bind-shader: expected shader");
2284 return SIGIL_UNDEFINED;
2286 if (sh->freed) {
2287 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
2288 "%bind-shader: shader has been freed");
2289 return SIGIL_UNDEFINED;
2291 sgp_set_pipeline(sh->pipeline);
2292 active_shader = sh;
2293 apply_active_shader_uniforms();
2294 return SIGIL_NIL;
2297/* (%unbind-shader) — restore default sgp pipeline + clear active shader.
2298 * sgp_reset_pipeline internally calls sgp_set_pipeline(INVALID), which
2299 * memsets the uniform buffer; calling sgp_reset_uniform afterwards
2300 * triggers an "invalid pipeline" assertion, so don't. */
2301static Value native_unbind_shader(SigilVM *vm, int argc, Value *args)
2303 (void)vm; (void)argc; (void)args;
2304 sgp_reset_pipeline();
2305 active_shader = NULL;
2306 return SIGIL_NIL;
2309/* ============================================================
2310 * MODULE INITIALIZATION
2311 * ============================================================ */
2313/* Forward declarations for related modules */
2314extern void sigil__init_sigil_graphics_image_module(SigilVM *vm);
2315extern void sigil__init_sigil_graphics_font_module(SigilVM *vm);
2317void sigil__init_sigil_graphics_module(SigilVM *vm)
2319 SigilModule *module = sigil_begin_module(vm, "(sigil graphics)");
2320 if (!module) return;
2322 /* Setup/shutdown */
2323 sigil_module_register_native(vm, "gfx-setup", native_gfx_setup,
2324 SIGIL_ARITY_EXACT(0), "Initialize graphics");
2325 sigil_module_register_native(vm, "gfx-shutdown", native_gfx_shutdown,
2326 SIGIL_ARITY_EXACT(0), "Shutdown graphics");
2327 sigil_module_register_native(vm, "gfx-initialized?", native_gfx_initialized,
2328 SIGIL_ARITY_EXACT(0), "Is graphics initialized?");
2330 /* Viewport */
2331 sigil_module_register_native(vm, "set-viewport", native_set_viewport,
2332 SIGIL_ARITY_RANGE(1, 2), "Set virtual viewport with letterboxing");
2333 sigil_module_register_native(vm, "set-letterbox-color", native_set_letterbox_color,
2334 SIGIL_ARITY_RANGE(3, 4), "Set letterbox bar color");
2336 /* Frame management */
2337 sigil_module_register_native(vm, "begin-frame", native_begin_frame,
2338 SIGIL_ARITY_EXACT(0), "Begin a new frame");
2339 sigil_module_register_native(vm, "end-frame", native_end_frame,
2340 SIGIL_ARITY_EXACT(0), "End current frame");
2342 /* Drawing */
2343 sigil_module_register_native(vm, "clear-screen", native_clear_screen,
2344 SIGIL_ARITY_RANGE(3, 4), "Set screen clear color");
2345 sigil_module_register_native(vm, "set-color", native_set_color,
2346 SIGIL_ARITY_RANGE(3, 4), "Set draw color");
2347 sigil_module_register_native(vm, "draw-filled-rect", native_draw_filled_rect,
2348 SIGIL_ARITY_EXACT(4), "Draw filled rectangle");
2349 sigil_module_register_native(vm, "draw-rect", native_draw_rect,
2350 SIGIL_ARITY_EXACT(4), "Draw rectangle outline");
2351 sigil_module_register_native(vm, "draw-line", native_draw_line,
2352 SIGIL_ARITY_EXACT(4), "Draw a line");
2353 sigil_module_register_native(vm, "draw-point", native_draw_point,
2354 SIGIL_ARITY_EXACT(2), "Draw a point");
2355 sigil_module_register_native(vm, "draw-triangle", native_draw_triangle,
2356 SIGIL_ARITY_EXACT(6), "Draw triangle outline");
2357 sigil_module_register_native(vm, "fill-triangle", native_fill_triangle,
2358 SIGIL_ARITY_EXACT(6), "Draw filled triangle");
2359 sigil_module_register_native(vm, "draw-filled-circle", native_draw_filled_circle,
2360 SIGIL_ARITY_RANGE(3, 4), "Draw filled circle");
2362 /* Blend mode (raw C primitives — wrapped by Sigil set-blend-mode for
2363 * tracking; users should prefer the Sigil wrapper or with-blend-mode.) */
2364 sigil_module_register_native(vm, "%set-blend-mode-native", native_set_blend_mode,
2365 SIGIL_ARITY_EXACT(1), "Set blend mode ('normal, 'additive, 'none)");
2366 sigil_module_register_native(vm, "%reset-blend-mode-native", native_reset_blend_mode,
2367 SIGIL_ARITY_EXACT(0), "Reset blend mode to sokol_gp default");
2369 /* Transform stack */
2370 sigil_module_register_native(vm, "push-transform", native_push_transform,
2371 SIGIL_ARITY_EXACT(0), "Save transform state");
2372 sigil_module_register_native(vm, "pop-transform", native_pop_transform,
2373 SIGIL_ARITY_EXACT(0), "Restore transform state");
2374 sigil_module_register_native(vm, "reset-transform", native_reset_transform,
2375 SIGIL_ARITY_EXACT(0), "Reset to identity transform");
2376 sigil_module_register_native(vm, "translate", native_translate,
2377 SIGIL_ARITY_EXACT(2), "Translate by (x, y)");
2378 sigil_module_register_native(vm, "rotate", native_rotate,
2379 SIGIL_ARITY_EXACT(1), "Rotate by angle (radians)");
2380 sigil_module_register_native(vm, "rotate-at", native_rotate_at,
2381 SIGIL_ARITY_EXACT(3), "Rotate around point");
2382 sigil_module_register_native(vm, "scale", native_scale,
2383 SIGIL_ARITY_EXACT(2), "Scale by (sx, sy)");
2384 sigil_module_register_native(vm, "scale-at", native_scale_at,
2385 SIGIL_ARITY_EXACT(4), "Scale around point");
2387 /* Texture functions */
2388 sigil_module_register_native(vm, "load-texture", native_load_texture,
2389 SIGIL_ARITY_EXACT(1), "Create GPU texture from image");
2390 sigil_module_register_native(vm, "texture?", native_texture_p,
2391 SIGIL_ARITY_EXACT(1), "Check if object is a texture");
2392 sigil_module_register_native(vm, "texture-width", native_texture_width,
2393 SIGIL_ARITY_EXACT(1), "Get texture width");
2394 sigil_module_register_native(vm, "texture-height", native_texture_height,
2395 SIGIL_ARITY_EXACT(1), "Get texture height");
2396 sigil_module_register_native(vm, "draw-texture", native_draw_texture,
2397 SIGIL_ARITY_RANGE(3, 5), "Draw texture at position");
2398 sigil_module_register_native(vm, "draw-texture-region", native_draw_texture_region,
2399 SIGIL_ARITY_EXACT(9), "Draw texture region");
2400 sigil_module_register_native(vm, "make-texture-from-pixels", native_make_texture_from_pixels,
2401 SIGIL_ARITY_RANGE(3, 4), "Create GPU texture from RGBA8 bytes");
2402 sigil_module_register_native(vm, "update-texture", native_update_texture,
2403 SIGIL_ARITY_EXACT(2), "Replace a pixel texture's RGBA8 bytes");
2405 /* Render targets — %begin-rt-pass / %end-rt-pass are raw primitives
2406 * wrapped by the with-render-target macro for stack discipline. */
2407 sigil_module_register_native(vm, "%make-render-target", native_make_render_target,
2408 SIGIL_ARITY_RANGE(2, 3), "Create offscreen render target");
2409 sigil_module_register_native(vm, "render-target?", native_render_target_p,
2410 SIGIL_ARITY_EXACT(1), "Check if object is a render target");
2411 sigil_module_register_native(vm, "render-target-width", native_render_target_width,
2412 SIGIL_ARITY_EXACT(1), "Get render target width");
2413 sigil_module_register_native(vm, "render-target-height", native_render_target_height,
2414 SIGIL_ARITY_EXACT(1), "Get render target height");
2415 sigil_module_register_native(vm, "render-target-free!", native_render_target_free,
2416 SIGIL_ARITY_EXACT(1), "Free render target GPU resources");
2417 sigil_module_register_native(vm, "%begin-rt-pass", native_begin_rt_pass,
2418 SIGIL_ARITY_EXACT(1), "Begin offscreen render pass");
2419 sigil_module_register_native(vm, "%end-rt-pass", native_end_rt_pass,
2420 SIGIL_ARITY_EXACT(0), "End offscreen render pass");
2421 sigil_module_register_native(vm, "render-target->texture", native_render_target_to_texture,
2422 SIGIL_ARITY_EXACT(1), "Borrow render target as texture");
2423 sigil_module_register_native(vm, "draw-render-target", native_draw_render_target,
2424 SIGIL_ARITY_EXACT(5), "Draw render target as textured quad");
2426 /* Shaders — custom fragment shaders for post-processing.
2427 * %bind-shader / %unbind-shader are raw primitives wrapped by the
2428 * with-shader macro for stack discipline + auto-uniform refresh. */
2429 sigil_module_register_native(vm, "load-shader", native_load_shader,
2430 SIGIL_ARITY_RANGE(2, 3), "Compile + link a shader from vertex+fragment GLSL");
2431 sigil_module_register_native(vm, "shader?", native_shader_p,
2432 SIGIL_ARITY_EXACT(1), "Check if object is a shader");
2433 sigil_module_register_native(vm, "shader-free!", native_shader_free,
2434 SIGIL_ARITY_EXACT(1), "Free shader GPU resources");
2435 sigil_module_register_native(vm, "set-shader-uniform", native_set_shader_uniform,
2436 SIGIL_ARITY_EXACT(3), "Set a uniform value on a shader");
2437 sigil_module_register_native(vm, "%bind-shader", native_bind_shader,
2438 SIGIL_ARITY_EXACT(1), "Bind a shader as the active pipeline");
2439 sigil_module_register_native(vm, "%unbind-shader", native_unbind_shader,
2440 SIGIL_ARITY_EXACT(0), "Unbind active shader, restore default");
2442 /* Export all */
2443 sigil_module_export(vm, "gfx-setup");
2444 sigil_module_export(vm, "gfx-shutdown");
2445 sigil_module_export(vm, "gfx-initialized?");
2446 sigil_module_export(vm, "set-viewport");
2447 sigil_module_export(vm, "set-letterbox-color");
2448 sigil_module_export(vm, "begin-frame");
2449 sigil_module_export(vm, "end-frame");
2450 sigil_module_export(vm, "clear-screen");
2451 sigil_module_export(vm, "set-color");
2452 sigil_module_export(vm, "draw-filled-rect");
2453 sigil_module_export(vm, "draw-rect");
2454 sigil_module_export(vm, "draw-line");
2455 sigil_module_export(vm, "draw-point");
2456 sigil_module_export(vm, "draw-triangle");
2457 sigil_module_export(vm, "fill-triangle");
2458 sigil_module_export(vm, "draw-filled-circle");
2459 sigil_module_export(vm, "%set-blend-mode-native");
2460 sigil_module_export(vm, "%reset-blend-mode-native");
2461 sigil_module_export(vm, "push-transform");
2462 sigil_module_export(vm, "pop-transform");
2463 sigil_module_export(vm, "reset-transform");
2464 sigil_module_export(vm, "translate");
2465 sigil_module_export(vm, "rotate");
2466 sigil_module_export(vm, "rotate-at");
2467 sigil_module_export(vm, "scale");
2468 sigil_module_export(vm, "scale-at");
2469 sigil_module_export(vm, "load-texture");
2470 sigil_module_export(vm, "texture?");
2471 sigil_module_export(vm, "texture-width");
2472 sigil_module_export(vm, "texture-height");
2473 sigil_module_export(vm, "draw-texture");
2474 sigil_module_export(vm, "draw-texture-region");
2475 sigil_module_export(vm, "make-texture-from-pixels");
2476 sigil_module_export(vm, "update-texture");
2477 sigil_module_export(vm, "%make-render-target");
2478 sigil_module_export(vm, "render-target?");
2479 sigil_module_export(vm, "render-target-width");
2480 sigil_module_export(vm, "render-target-height");
2481 sigil_module_export(vm, "render-target-free!");
2482 sigil_module_export(vm, "%begin-rt-pass");
2483 sigil_module_export(vm, "%end-rt-pass");
2484 sigil_module_export(vm, "render-target->texture");
2485 sigil_module_export(vm, "draw-render-target");
2486 sigil_module_export(vm, "load-shader");
2487 sigil_module_export(vm, "shader?");
2488 sigil_module_export(vm, "shader-free!");
2489 sigil_module_export(vm, "set-shader-uniform");
2490 sigil_module_export(vm, "%bind-shader");
2491 sigil_module_export(vm, "%unbind-shader");
2493 sigil_end_module(vm);
2495 /* Initialize related modules */
2496 sigil__init_sigil_graphics_image_module(vm);
2497 sigil__init_sigil_graphics_font_module(vm);