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 * On web (wasm32-wasi) there is no windowing system: sokol_app / sokol_glue are
12 * replaced by the sigil-wasm-gles3 JS bridge (canvas + WebGL2 context +
13 * swapchain). Those two headers are therefore excluded on the web build; every
14 * sapp / sglue touch point is routed through the sig_gfx_ abstraction below,
15 * whose native branch expands to the exact same calls as before. */
16#if !defined(__wasm__)
17#include "sokol_app.h"
18#endif
19#include "sokol_gfx.h"
20#if !defined(__wasm__)
21#include "sokol_glue.h"
22#endif
23#include "sokol_gp.h"
24#include "sokol_log.h"
26#include <stdio.h>
27#include <stdlib.h>
28#include <stdbool.h>
29#include <string.h>
30#include <math.h>
31#include <time.h>
32#include <stdint.h>
34/* ---- platform abstraction: native (sokol_app/glue) vs web (GL bridge) -------
35 * These four accessors are the ONLY sokol_app/sokol_glue touch points in this
36 * file. Native expands to the exact sapp / sglue calls used before, so the
37 * native build is behaviour-identical. Web supplies the default framebuffer
38 * swapchain manually (lifted from the Milestone-1 wasm-sokol-sprite example) and
39 * sources the canvas size from the sigil_wasm_gles3 app-shell import module. */
40#if defined(__wasm__)
41__attribute__((import_module("sigil_wasm_gles3"), import_name("canvas_width")))
42extern int sigil_wasm_gles3_canvas_width(void);
43__attribute__((import_module("sigil_wasm_gles3"), import_name("canvas_height")))
44extern int sigil_wasm_gles3_canvas_height(void);
46static int sig_gfx_width(void) { return sigil_wasm_gles3_canvas_width(); }
47static int sig_gfx_height(void) { return sigil_wasm_gles3_canvas_height(); }
49static sg_environment sig_gfx_environment(void) {
50 sg_environment env = {0};
51 env.defaults.color_format = SG_PIXELFORMAT_RGBA8;
52 env.defaults.depth_format = SG_PIXELFORMAT_NONE;
53 env.defaults.sample_count = 1;
54 return env;
56static sg_swapchain sig_gfx_swapchain(void) {
57 sg_swapchain swap = {0};
58 swap.width = sig_gfx_width();
59 swap.height = sig_gfx_height();
60 swap.sample_count = 1;
61 swap.color_format = SG_PIXELFORMAT_RGBA8;
62 swap.depth_format = SG_PIXELFORMAT_NONE;
63 swap.gl.framebuffer = 0; /* default framebuffer */
64 return swap;
66#else
67static inline int sig_gfx_width(void) { return sapp_width(); }
68static inline int sig_gfx_height(void) { return sapp_height(); }
69static inline sg_environment sig_gfx_environment(void) { return sglue_environment(); }
70static inline sg_swapchain sig_gfx_swapchain(void) { return sglue_swapchain(); }
71#endif
73#ifndef M_PI
74#define M_PI 3.14159265358979323846
75#endif
77/* Maximum segments for a circle triangle fan. Stack-allocated buffer is sized
78 * to this. Higher values produce smoother circles at the cost of more triangles
79 * per draw call. 64 is plenty for typical bullet/UI usage. */
80#define SIGIL_GFX_CIRCLE_MAX_SEGMENTS 128
82/* External: get pixel data from image (defined in image.c) */
83extern unsigned char *sigil_graphics_image_pixels(SigilVM *vm, Value img_val, int *width, int *height);
85/* Graphics initialized flag */
86static bool gfx_initialized = false;
88/* Texture type tag (initialized at module init) */
89static Value texture_type_tag = SIGIL_UNDEFINED;
91/* Render target type tag (initialized at module init) */
92static Value rt_type_tag = SIGIL_UNDEFINED;
94/* Shader type tag (initialized at module init) */
95static Value shader_type_tag = SIGIL_UNDEFINED;
97/* Texture structure.
98 *
99 * `owns_resources` is false for textures that borrow their handles from
100 * another owner (e.g., from a render target via render-target->texture).
101 * Borrowed wrappers must not destroy the underlying sokol resources;
102 * the original owner does that. */
103typedef struct {
104 sg_image handle;
105 sg_sampler sampler;
106 sg_view view;
107 int width;
108 int height;
109 bool owns_resources;
110} GfxTexture;
112/* Render target structure.
113 *
114 * Holds a color image, a color-attachment view (for rendering INTO),
115 * a texture view (for sampling AS texture), a sampler, and a
116 * depth-stencil image + view. The depth-stencil attachment is only
117 * needed because sgp's default pipelines bake in the swap chain's
118 * depth-stencil format; offscreen passes must provide a matching
119 * attachment so pipeline validation passes. We don't actually use the
120 * depth buffer for 2D rendering. */
121typedef struct {
122 sg_image color_img;
123 sg_view color_att_view;
124 sg_view tex_view;
125 sg_sampler sampler;
126 sg_image depth_img;
127 sg_view depth_att_view;
128 int width;
129 int height;
130 bool freed;
131} GfxRenderTarget;
133/* Shader uniform entry — one per uniform declared in the user's
134 * fragment GLSL. The `offset` is into the per-shader uniform buffer;
135 * sgp_set_uniform ships the buffer contents to the GPU per-draw. */
136#define SIGIL_GFX_MAX_UNIFORMS 16
137#define SIGIL_GFX_UNIFORM_BUFFER_SIZE 256 /* matches SGP_UNIFORM_CONTENT_SLOTS=64 floats */
138#define SIGIL_GFX_UNIFORM_NAME_MAX 64
140typedef struct {
141 char name[SIGIL_GFX_UNIFORM_NAME_MAX];
142 sg_uniform_type type;
143 uint32_t offset;
144 uint32_t size;
145} GfxUniformEntry;
147/* Shader structure.
148 *
149 * Holds the compiled sokol shader, the sgp pipeline that bakes it in,
150 * the parsed uniform layout (name → offset/type/size), and a CPU
151 * buffer that mirrors the GPU uniform block. Auto-uniforms u_time and
152 * u_resolution are tracked by index for fast per-draw refresh.
153 *
154 * Texture sampler bindings (sampler2D uniforms) are NOT tracked here —
155 * channel 0 is bound by the draw primitive itself (e.g.,
156 * draw-render-target sets channel 0 to the source texture). */
157typedef struct {
158 sg_shader shader;
159 sg_pipeline pipeline;
160 GfxUniformEntry uniforms[SIGIL_GFX_MAX_UNIFORMS];
161 int num_uniforms;
162 uint8_t buffer[SIGIL_GFX_UNIFORM_BUFFER_SIZE];
163 uint32_t buffer_size;
164 int u_time_index; /* -1 if shader doesn't use u_time */
165 int u_resolution_index; /* -1 if shader doesn't use u_resolution */
166 bool freed;
167} GfxShader;
169/* Pointer to the currently active shader (set by %bind-shader,
170 * cleared by %unbind-shader). Used by the auto-uniform refresh path
171 * during draw-render-target / draw-texture so u_time advances and
172 * u_resolution tracks viewport size without caller intervention. */
173static GfxShader *active_shader = NULL;
175/* Process start time for u_time. Set on first sg_setup. */
176static double gfx_start_time = 0.0;
178/* Current draw color */
179static float draw_color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
181/* Clear color (set by clear, used in end-frame) */
182static float clear_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
184/* SGP initialized flag */
185static bool sgp_initialized = false;
187/* Virtual viewport state */
188static bool virtual_viewport_enabled = false;
189static int virtual_width = 0;
190static int virtual_height = 0;
192/* Letterbox color (bars outside viewport) */
193static float letterbox_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
195/* Helper to extract float from fixnum or flonum */
196static float value_to_float(Value v)
198 if (sigil_is_fixnum(v)) {
199 return (float)sigil_as_fixnum(v);
200 } else if (sigil_is_flonum(v)) {
201 return (float)sigil_as_flonum(v);
202 }
203 return 0.0f;
206/* Monotonic seconds since gfx-setup. Used by the shader auto-uniform
207 * u_time. clock_gettime(CLOCK_MONOTONIC) is unaffected by wall-clock
208 * jumps and has nanosecond resolution. */
209static float gfx_elapsed_seconds(void)
211 struct timespec ts;
212 clock_gettime(CLOCK_MONOTONIC, &ts);
213 double now = (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
214 return (float)(now - gfx_start_time);
217/* ============================================================
218 * NATIVE FUNCTIONS
219 * ============================================================ */
221/*
222 * (gfx-setup) - Initialize graphics subsystem
223 * Must be called in the app-run init callback after window is created.
224 */
225static Value native_gfx_setup(SigilVM *vm, int argc, Value *args)
227 (void)vm; (void)argc; (void)args;
229 if (gfx_initialized) {
230 return SIGIL_NIL;
231 }
233 /* Initialize sokol_gfx. Install slog_func so validation failures
234 * print a useful diagnostic before sokol aborts. */
235 sg_desc desc = {
236 .environment = sig_gfx_environment(),
237 .logger.func = slog_func,
238 };
239 sg_setup(&desc);
241 /* Capture process start time for the shader u_time auto-uniform. */
242 {
243 struct timespec ts;
244 clock_gettime(CLOCK_MONOTONIC, &ts);
245 gfx_start_time = (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
246 }
248 /* Initialize sokol_gp for 2D rendering */
249 sgp_desc sgpdesc = {0};
250 sgp_setup(&sgpdesc);
251 if (!sgp_is_valid()) {
252 fprintf(stderr, "Failed to initialize sokol_gp\n");
253 sg_shutdown();
254 return SIGIL_FALSE;
255 }
256 sgp_initialized = true;
257 gfx_initialized = true;
259 return SIGIL_NIL;
262/*
263 * (gfx-shutdown) - Shutdown graphics subsystem
264 */
265static Value native_gfx_shutdown(SigilVM *vm, int argc, Value *args)
267 (void)vm; (void)argc; (void)args;
269 if (gfx_initialized) {
270 if (sgp_initialized) {
271 sgp_shutdown();
272 sgp_initialized = false;
273 }
274 sg_shutdown();
275 gfx_initialized = false;
276 }
278 return SIGIL_NIL;
281/*
282 * (set-letterbox-color r g b [a]) - Set the color for letterbox bars
283 *
284 * Default is black. Only visible when using a virtual viewport.
285 */
286static Value native_set_letterbox_color(SigilVM *vm, int argc, Value *args)
288 if (argc < 3) {
289 sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-letterbox-color: requires r, g, b arguments");
290 return SIGIL_UNDEFINED;
291 }
293 letterbox_color[0] = value_to_float(args[0]);
294 letterbox_color[1] = value_to_float(args[1]);
295 letterbox_color[2] = value_to_float(args[2]);
296 letterbox_color[3] = argc > 3 ? value_to_float(args[3]) : 1.0f;
298 return SIGIL_NIL;
301/*
302 * (set-viewport width height) - Set virtual viewport with letterboxing
303 *
304 * Creates a fixed coordinate space that maintains aspect ratio.
305 * Black bars are added as needed to fill the window.
306 * Call with #f to disable and use window coordinates.
307 */
308static Value native_set_viewport(SigilVM *vm, int argc, Value *args)
310 (void)vm;
312 if (argc == 1 && sigil_is_false(args[0])) {
313 /* Disable virtual viewport */
314 virtual_viewport_enabled = false;
315 return SIGIL_NIL;
316 }
318 if (argc < 2) {
319 sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-viewport: requires width, height");
320 return SIGIL_UNDEFINED;
321 }
323 virtual_viewport_enabled = true;
324 virtual_width = (int)value_to_float(args[0]);
325 virtual_height = (int)value_to_float(args[1]);
327 return SIGIL_NIL;
330/*
331 * (begin-frame) - Begin a new frame
332 */
333static Value native_begin_frame(SigilVM *vm, int argc, Value *args)
335 (void)vm; (void)argc; (void)args;
337 int window_w = sig_gfx_width();
338 int window_h = sig_gfx_height();
340 /* Begin sokol_gp frame with full window size */
341 sgp_begin(window_w, window_h);
343 if (virtual_viewport_enabled && virtual_width > 0 && virtual_height > 0) {
344 /* Calculate letterbox viewport */
345 float scale_x = (float)window_w / (float)virtual_width;
346 float scale_y = (float)window_h / (float)virtual_height;
347 float scale = (scale_x < scale_y) ? scale_x : scale_y;
349 int viewport_w = (int)(virtual_width * scale);
350 int viewport_h = (int)(virtual_height * scale);
351 int viewport_x = (window_w - viewport_w) / 2;
352 int viewport_y = (window_h - viewport_h) / 2;
354 sgp_viewport(viewport_x, viewport_y, viewport_w, viewport_h);
355 sgp_project(0, (float)virtual_width, 0, (float)virtual_height);
356 } else {
357 /* Default: use window coordinates */
358 sgp_viewport(0, 0, window_w, window_h);
359 sgp_project(0, (float)window_w, 0, (float)window_h);
360 }
362 /* Reset to white draw color */
363 sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
365 return SIGIL_NIL;
368/*
369 * (end-frame) - End the current frame
370 */
371static Value native_end_frame(SigilVM *vm, int argc, Value *args)
373 (void)vm; (void)argc; (void)args;
375 /* Begin render pass - clear to letterbox color */
376 sg_pass_action pass_action = {
377 .colors[0] = {
378 .load_action = SG_LOADACTION_CLEAR,
379 .clear_value = {letterbox_color[0], letterbox_color[1],
380 letterbox_color[2], letterbox_color[3]}
381 }
382 };
383 sg_pass pass = {
384 .action = pass_action,
385 .swapchain = sig_gfx_swapchain()
386 };
387 sg_begin_pass(&pass);
389 /* Flush sokol_gp commands to GPU */
390 sgp_flush();
391 sgp_end();
393 sg_end_pass();
394 sg_commit();
396 return SIGIL_NIL;
399/*
400 * (clear-screen r g b [a]) - Clear the viewport with a color
401 *
402 * When using a virtual viewport, this fills the viewport area.
403 * The letterbox bars remain the pass clear color (black).
404 */
405static Value native_clear_screen(SigilVM *vm, int argc, Value *args)
407 if (argc < 3) {
408 sigil__vm_error(vm, SIGIL_ERR_ARITY, "clear-screen: requires r, g, b arguments");
409 return SIGIL_UNDEFINED;
410 }
412 float r = value_to_float(args[0]);
413 float g = value_to_float(args[1]);
414 float b = value_to_float(args[2]);
415 float a = argc > 3 ? value_to_float(args[3]) : 1.0f;
417 /* Draw a filled rectangle covering the entire viewport/projection area */
418 sgp_set_color(r, g, b, a);
419 if (virtual_viewport_enabled && virtual_width > 0 && virtual_height > 0) {
420 sgp_draw_filled_rect(0, 0, (float)virtual_width, (float)virtual_height);
421 } else {
422 sgp_draw_filled_rect(0, 0, (float)sig_gfx_width(), (float)sig_gfx_height());
423 }
425 /* Reset to white for subsequent drawing */
426 sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f);
428 return SIGIL_NIL;
431/*
432 * (set-color r g b [a]) - Set current draw color
433 */
434static Value native_set_color(SigilVM *vm, int argc, Value *args)
436 if (argc < 3) {
437 sigil__vm_error(vm, SIGIL_ERR_ARITY, "set-color: requires r, g, b arguments");
438 return SIGIL_UNDEFINED;
439 }
441 draw_color[0] = value_to_float(args[0]);
442 draw_color[1] = value_to_float(args[1]);
443 draw_color[2] = value_to_float(args[2]);
444 draw_color[3] = argc > 3 ? value_to_float(args[3]) : 1.0f;
446 /* Set sokol_gp color */
447 sgp_set_color(draw_color[0], draw_color[1], draw_color[2], draw_color[3]);
449 return SIGIL_NIL;
452/*
453 * (draw-filled-rect x y w h) - Draw a filled rectangle
454 */
455static Value native_draw_filled_rect(SigilVM *vm, int argc, Value *args)
457 if (argc < 4) {
458 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-filled-rect: requires x, y, w, h arguments");
459 return SIGIL_UNDEFINED;
460 }
462 float x = value_to_float(args[0]);
463 float y = value_to_float(args[1]);
464 float w = value_to_float(args[2]);
465 float h = value_to_float(args[3]);
467 sgp_draw_filled_rect(x, y, w, h);
469 return SIGIL_NIL;
472/*
473 * (draw-rect x y w h) - Draw a rectangle outline
474 */
475static Value native_draw_rect(SigilVM *vm, int argc, Value *args)
477 if (argc < 4) {
478 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-rect: requires x, y, w, h arguments");
479 return SIGIL_UNDEFINED;
480 }
482 float x = value_to_float(args[0]);
483 float y = value_to_float(args[1]);
484 float w = value_to_float(args[2]);
485 float h = value_to_float(args[3]);
487 /* Draw rectangle outline using 4 lines */
488 sgp_line lines[4] = {
489 {{x, y}, {x + w, y}}, /* top */
490 {{x + w, y}, {x + w, y + h}}, /* right */
491 {{x + w, y + h}, {x, y + h}}, /* bottom */
492 {{x, y + h}, {x, y}} /* left */
493 };
494 sgp_draw_lines(lines, 4);
496 return SIGIL_NIL;
499/*
500 * (draw-line x1 y1 x2 y2) - Draw a line
501 */
502static Value native_draw_line(SigilVM *vm, int argc, Value *args)
504 if (argc < 4) {
505 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-line: requires x1, y1, x2, y2 arguments");
506 return SIGIL_UNDEFINED;
507 }
509 float x1 = value_to_float(args[0]);
510 float y1 = value_to_float(args[1]);
511 float x2 = value_to_float(args[2]);
512 float y2 = value_to_float(args[3]);
514 sgp_line line = {{x1, y1}, {x2, y2}};
515 sgp_draw_lines(&line, 1);
517 return SIGIL_NIL;
520/*
521 * (draw-point x y) - Draw a single point
522 */
523static Value native_draw_point(SigilVM *vm, int argc, Value *args)
525 if (argc < 2) {
526 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-point: requires x, y arguments");
527 return SIGIL_UNDEFINED;
528 }
530 float x = value_to_float(args[0]);
531 float y = value_to_float(args[1]);
533 sgp_point pt = {x, y};
534 sgp_draw_points(&pt, 1);
536 return SIGIL_NIL;
539/*
540 * (draw-triangle x1 y1 x2 y2 x3 y3) - Draw a triangle outline
541 */
542static Value native_draw_triangle(SigilVM *vm, int argc, Value *args)
544 if (argc < 6) {
545 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-triangle: requires x1, y1, x2, y2, x3, y3");
546 return SIGIL_UNDEFINED;
547 }
549 float x1 = value_to_float(args[0]);
550 float y1 = value_to_float(args[1]);
551 float x2 = value_to_float(args[2]);
552 float y2 = value_to_float(args[3]);
553 float x3 = value_to_float(args[4]);
554 float y3 = value_to_float(args[5]);
556 sgp_line lines[3] = {
557 {{x1, y1}, {x2, y2}},
558 {{x2, y2}, {x3, y3}},
559 {{x3, y3}, {x1, y1}}
560 };
561 sgp_draw_lines(lines, 3);
563 return SIGIL_NIL;
566/*
567 * (fill-triangle x1 y1 x2 y2 x3 y3) - Draw a filled triangle
568 */
569static Value native_fill_triangle(SigilVM *vm, int argc, Value *args)
571 if (argc < 6) {
572 sigil__vm_error(vm, SIGIL_ERR_ARITY, "fill-triangle: requires x1, y1, x2, y2, x3, y3");
573 return SIGIL_UNDEFINED;
574 }
576 float x1 = value_to_float(args[0]);
577 float y1 = value_to_float(args[1]);
578 float x2 = value_to_float(args[2]);
579 float y2 = value_to_float(args[3]);
580 float x3 = value_to_float(args[4]);
581 float y3 = value_to_float(args[5]);
583 sgp_triangle tri = {{x1, y1}, {x2, y2}, {x3, y3}};
584 sgp_draw_filled_triangles(&tri, 1);
586 return SIGIL_NIL;
589/*
590 * (draw-filled-circle x y radius [segments]) - Draw a filled circle
591 *
592 * Renders the circle as a triangle fan around (x, y). `segments` controls
593 * the polygon resolution; default is 16. For tiny bullets (radius 4-6 px),
594 * 12-16 is plenty. Larger circles benefit from more segments. Clamped to
595 * [3, SIGIL_GFX_CIRCLE_MAX_SEGMENTS].
596 */
597static Value native_draw_filled_circle(SigilVM *vm, int argc, Value *args)
599 if (argc < 3) {
600 sigil__vm_error(vm, SIGIL_ERR_ARITY,
601 "draw-filled-circle: requires x, y, radius arguments");
602 return SIGIL_UNDEFINED;
603 }
605 float cx = value_to_float(args[0]);
606 float cy = value_to_float(args[1]);
607 float radius = value_to_float(args[2]);
609 int segments = 16;
610 if (argc > 3) {
611 segments = (int)value_to_float(args[3]);
612 }
613 if (segments < 3) segments = 3;
614 if (segments > SIGIL_GFX_CIRCLE_MAX_SEGMENTS) {
615 segments = SIGIL_GFX_CIRCLE_MAX_SEGMENTS;
616 }
618 if (radius <= 0.0f) {
619 return SIGIL_NIL;
620 }
622 sgp_triangle tris[SIGIL_GFX_CIRCLE_MAX_SEGMENTS];
623 float step = (float)(2.0 * M_PI) / (float)segments;
624 float prev_x = cx + radius;
625 float prev_y = cy;
626 for (int i = 1; i <= segments; ++i) {
627 float angle = step * (float)i;
628 float nx = cx + radius * cosf(angle);
629 float ny = cy + radius * sinf(angle);
630 tris[i - 1].a.x = cx; tris[i - 1].a.y = cy;
631 tris[i - 1].b.x = prev_x; tris[i - 1].b.y = prev_y;
632 tris[i - 1].c.x = nx; tris[i - 1].c.y = ny;
633 prev_x = nx;
634 prev_y = ny;
635 }
637 sgp_draw_filled_triangles(tris, (uint32_t)segments);
639 return SIGIL_NIL;
642/* ============================================================
643 * BLEND MODES
644 * ============================================================ */
646/* Map a Sigil symbol value to an sgp_blend_mode. Returns -1 if unknown. */
647static int blend_mode_from_value(Value v)
649 if (sigil_is_symbol(v)) {
650 const char *name = sigil_symbol_name(v);
651 if (name) {
652 if (strcmp(name, "normal") == 0) return SGP_BLENDMODE_BLEND;
653 if (strcmp(name, "additive") == 0) return SGP_BLENDMODE_ADD;
654 if (strcmp(name, "none") == 0) return SGP_BLENDMODE_NONE;
655 }
656 }
657 return -1;
660/*
661 * (set-blend-mode mode) - Set the current blend mode
662 *
663 * mode is one of: 'normal (alpha blend), 'additive, 'none.
664 * Stays in effect until changed or reset-blend-mode is called.
665 */
666static Value native_set_blend_mode(SigilVM *vm, int argc, Value *args)
668 if (argc < 1) {
669 sigil__vm_error(vm, SIGIL_ERR_ARITY,
670 "set-blend-mode: requires mode symbol");
671 return SIGIL_UNDEFINED;
672 }
673 int mode = blend_mode_from_value(args[0]);
674 if (mode < 0) {
675 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
676 "set-blend-mode: expected 'normal, 'additive, or 'none");
677 return SIGIL_UNDEFINED;
678 }
679 sgp_set_blend_mode((sgp_blend_mode)mode);
680 return SIGIL_NIL;
683/*
684 * (reset-blend-mode) - Reset blend mode to sokol_gp default (no blending)
685 */
686static Value native_reset_blend_mode(SigilVM *vm, int argc, Value *args)
688 (void)vm; (void)argc; (void)args;
689 sgp_reset_blend_mode();
690 return SIGIL_NIL;
693/* ============================================================
694 * TRANSFORM STACK
695 * ============================================================ */
697/*
698 * (push-transform) - Save current transform state
699 */
700static Value native_push_transform(SigilVM *vm, int argc, Value *args)
702 (void)vm; (void)argc; (void)args;
703 sgp_push_transform();
704 return SIGIL_NIL;
707/*
708 * (pop-transform) - Restore previous transform state
709 */
710static Value native_pop_transform(SigilVM *vm, int argc, Value *args)
712 (void)vm; (void)argc; (void)args;
713 sgp_pop_transform();
714 return SIGIL_NIL;
717/*
718 * (reset-transform) - Reset to identity transform
719 */
720static Value native_reset_transform(SigilVM *vm, int argc, Value *args)
722 (void)vm; (void)argc; (void)args;
723 sgp_reset_transform();
724 return SIGIL_NIL;
727/*
728 * (translate x y) - Translate by (x, y)
729 */
730static Value native_translate(SigilVM *vm, int argc, Value *args)
732 if (argc < 2) {
733 sigil__vm_error(vm, SIGIL_ERR_ARITY, "translate: requires x, y arguments");
734 return SIGIL_UNDEFINED;
735 }
737 float x = value_to_float(args[0]);
738 float y = value_to_float(args[1]);
740 sgp_translate(x, y);
742 return SIGIL_NIL;
745/*
746 * (rotate angle) - Rotate by angle (in radians)
747 */
748static Value native_rotate(SigilVM *vm, int argc, Value *args)
750 if (argc < 1) {
751 sigil__vm_error(vm, SIGIL_ERR_ARITY, "rotate: requires angle argument");
752 return SIGIL_UNDEFINED;
753 }
755 float angle = value_to_float(args[0]);
756 sgp_rotate(angle);
758 return SIGIL_NIL;
761/*
762 * (rotate-at angle x y) - Rotate around point (x, y)
763 */
764static Value native_rotate_at(SigilVM *vm, int argc, Value *args)
766 if (argc < 3) {
767 sigil__vm_error(vm, SIGIL_ERR_ARITY, "rotate-at: requires angle, x, y arguments");
768 return SIGIL_UNDEFINED;
769 }
771 float angle = value_to_float(args[0]);
772 float x = value_to_float(args[1]);
773 float y = value_to_float(args[2]);
775 sgp_rotate_at(angle, x, y);
777 return SIGIL_NIL;
780/*
781 * (scale sx sy) - Scale by (sx, sy)
782 */
783static Value native_scale(SigilVM *vm, int argc, Value *args)
785 if (argc < 2) {
786 sigil__vm_error(vm, SIGIL_ERR_ARITY, "scale: requires sx, sy arguments");
787 return SIGIL_UNDEFINED;
788 }
790 float sx = value_to_float(args[0]);
791 float sy = value_to_float(args[1]);
793 sgp_scale(sx, sy);
795 return SIGIL_NIL;
798/*
799 * (scale-at sx sy x y) - Scale around point (x, y)
800 */
801static Value native_scale_at(SigilVM *vm, int argc, Value *args)
803 if (argc < 4) {
804 sigil__vm_error(vm, SIGIL_ERR_ARITY, "scale-at: requires sx, sy, x, y arguments");
805 return SIGIL_UNDEFINED;
806 }
808 float sx = value_to_float(args[0]);
809 float sy = value_to_float(args[1]);
810 float x = value_to_float(args[2]);
811 float y = value_to_float(args[3]);
813 sgp_scale_at(sx, sy, x, y);
815 return SIGIL_NIL;
818/*
819 * (gfx-initialized?) -> boolean
820 */
821static Value native_gfx_initialized(SigilVM *vm, int argc, Value *args)
823 (void)vm; (void)argc; (void)args;
824 return gfx_initialized ? SIGIL_TRUE : SIGIL_FALSE;
827/* ============================================================
828 * TEXTURE FUNCTIONS
829 * ============================================================ */
831/* Initialize texture type tag */
832static void ensure_texture_type(SigilVM *vm)
834 if (sigil_is_undefined(texture_type_tag)) {
835 texture_type_tag = sigil_intern_symbol(vm, "sigil-graphics-texture", 22);
836 }
839/* Get texture from Value, returns NULL if not a texture */
840static GfxTexture *get_texture(SigilVM *vm, Value v)
842 if (!sigil_is_foreign(v)) return NULL;
843 ensure_texture_type(vm);
844 if (sigil_foreign_type(v) != texture_type_tag) return NULL;
845 return (GfxTexture *)sigil_foreign_data(v);
848/* Texture destructor */
849static void texture_destructor(void *data)
851 GfxTexture *tex = (GfxTexture *)data;
852 if (tex) {
853 if (tex->owns_resources) {
854 if (tex->view.id != SG_INVALID_ID) {
855 sg_destroy_view(tex->view);
856 }
857 if (tex->handle.id != SG_INVALID_ID) {
858 sg_destroy_image(tex->handle);
859 }
860 if (tex->sampler.id != SG_INVALID_ID) {
861 sg_destroy_sampler(tex->sampler);
862 }
863 }
864 free(tex);
865 }
868/*
869 * (load-texture image) -> <texture> or #f
870 *
871 * Create a GPU texture from a CPU-side image.
872 */
873static Value native_load_texture(SigilVM *vm, int argc, Value *args)
875 (void)argc;
877 int width, height;
878 unsigned char *pixels = sigil_graphics_image_pixels(vm, args[0], &width, &height);
880 if (!pixels) {
881 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
882 "load-texture: expected valid image with pixel data");
883 return SIGIL_FALSE;
884 }
886 /* Create sokol image */
887 sg_image_desc img_desc = {
888 .width = width,
889 .height = height,
890 .pixel_format = SG_PIXELFORMAT_RGBA8,
891 .data.mip_levels[0] = {
892 .ptr = pixels,
893 .size = (size_t)(width * height * 4)
894 }
895 };
896 sg_image img = sg_make_image(&img_desc);
898 if (img.id == SG_INVALID_ID) {
899 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
900 "load-texture: failed to create GPU texture");
901 return SIGIL_FALSE;
902 }
904 /* Create sampler with default settings (linear filtering, clamp) */
905 sg_sampler_desc smp_desc = {
906 .min_filter = SG_FILTER_LINEAR,
907 .mag_filter = SG_FILTER_LINEAR,
908 .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
909 .wrap_v = SG_WRAP_CLAMP_TO_EDGE
910 };
911 sg_sampler smp = sg_make_sampler(&smp_desc);
913 /* Create texture view from image (required by new sokol_gp API) */
914 sg_view view = sgp_make_texture_view_from_image(img, "sigil-texture");
915 if (view.id == SG_INVALID_ID) {
916 sg_destroy_image(img);
917 sg_destroy_sampler(smp);
918 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
919 "load-texture: failed to create texture view");
920 return SIGIL_FALSE;
921 }
923 /* Create texture structure */
924 GfxTexture *tex = malloc(sizeof(GfxTexture));
925 if (!tex) {
926 sg_destroy_view(view);
927 sg_destroy_image(img);
928 sg_destroy_sampler(smp);
929 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY, "load-texture: out of memory");
930 return SIGIL_FALSE;
931 }
933 tex->handle = img;
934 tex->sampler = smp;
935 tex->view = view;
936 tex->width = width;
937 tex->height = height;
938 tex->owns_resources = true;
940 ensure_texture_type(vm);
941 return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
942 sizeof(GfxTexture));
945/*
946 * (texture? obj) -> boolean
947 */
948static Value native_texture_p(SigilVM *vm, int argc, Value *args)
950 (void)argc;
951 return get_texture(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
954/*
955 * (texture-width tex) -> integer
956 */
957static Value native_texture_width(SigilVM *vm, int argc, Value *args)
959 (void)argc;
960 GfxTexture *tex = get_texture(vm, args[0]);
961 if (!tex) {
962 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "texture-width: expected texture");
963 return SIGIL_UNDEFINED;
964 }
965 return sigil_fixnum(tex->width);
968/*
969 * (texture-height tex) -> integer
970 */
971static Value native_texture_height(SigilVM *vm, int argc, Value *args)
973 (void)argc;
974 GfxTexture *tex = get_texture(vm, args[0]);
975 if (!tex) {
976 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "texture-height: expected texture");
977 return SIGIL_UNDEFINED;
978 }
979 return sigil_fixnum(tex->height);
982/*
983 * (draw-texture tex x y [w h]) - Draw texture at position
984 *
985 * If w/h are not provided, uses texture's native size.
986 */
987static Value native_draw_texture(SigilVM *vm, int argc, Value *args)
989 if (argc < 3) {
990 sigil__vm_error(vm, SIGIL_ERR_ARITY, "draw-texture: requires texture, x, y arguments");
991 return SIGIL_UNDEFINED;
992 }
994 GfxTexture *tex = get_texture(vm, args[0]);
995 if (!tex) {
996 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-texture: expected texture");
997 return SIGIL_UNDEFINED;
998 }
1000 float x = value_to_float(args[1]);
1001 float y = value_to_float(args[2]);
1002 float w = argc > 3 ? value_to_float(args[3]) : (float)tex->width;
1003 float h = argc > 4 ? value_to_float(args[4]) : (float)tex->height;
1005 /* Bind texture view and sampler */
1006 sgp_set_view(0, tex->view);
1007 sgp_set_sampler(0, tex->sampler);
1009 /* Draw textured rectangle - source is entire texture */
1010 sgp_rect dest = {x, y, w, h};
1011 sgp_rect src = {0, 0, (float)tex->width, (float)tex->height};
1012 sgp_draw_textured_rect(0, dest, src);
1014 /* Reset to default (white texture) */
1015 sgp_reset_view(0);
1016 sgp_reset_sampler(0);
1018 return SIGIL_NIL;
1022 * (draw-texture-region tex x y w h sx sy sw sh) - Draw portion of texture
1024 * Draws source region (sx, sy, sw, sh) from texture to destination (x, y, w, h).
1025 */
1026static Value native_draw_texture_region(SigilVM *vm, int argc, Value *args)
1028 if (argc < 9) {
1029 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1030 "draw-texture-region: requires texture, x, y, w, h, sx, sy, sw, sh");
1031 return SIGIL_UNDEFINED;
1034 GfxTexture *tex = get_texture(vm, args[0]);
1035 if (!tex) {
1036 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "draw-texture-region: expected texture");
1037 return SIGIL_UNDEFINED;
1040 float x = value_to_float(args[1]);
1041 float y = value_to_float(args[2]);
1042 float w = value_to_float(args[3]);
1043 float h = value_to_float(args[4]);
1044 float sx = value_to_float(args[5]);
1045 float sy = value_to_float(args[6]);
1046 float sw = value_to_float(args[7]);
1047 float sh = value_to_float(args[8]);
1049 /* Bind texture view and sampler */
1050 sgp_set_view(0, tex->view);
1051 sgp_set_sampler(0, tex->sampler);
1053 /* Draw textured rectangle with source region */
1054 sgp_rect dest = {x, y, w, h};
1055 sgp_rect src = {sx, sy, sw, sh};
1056 sgp_draw_textured_rect(0, dest, src);
1058 /* Reset to default */
1059 sgp_reset_view(0);
1060 sgp_reset_sampler(0);
1062 return SIGIL_NIL;
1065/* ============================================================
1066 * RENDER TARGETS
1067 * ============================================================ */
1069static void ensure_rt_type(SigilVM *vm)
1071 if (sigil_is_undefined(rt_type_tag)) {
1072 rt_type_tag = sigil_intern_symbol(vm, "sigil-graphics-rt", 17);
1076static GfxRenderTarget *get_rt(SigilVM *vm, Value v)
1078 if (!sigil_is_foreign(v)) return NULL;
1079 ensure_rt_type(vm);
1080 if (sigil_foreign_type(v) != rt_type_tag) return NULL;
1081 return (GfxRenderTarget *)sigil_foreign_data(v);
1084/* Free a render target's GPU resources. Idempotent. */
1085static void rt_free_resources(GfxRenderTarget *rt)
1087 if (!rt || rt->freed) return;
1088 if (rt->color_att_view.id != SG_INVALID_ID) {
1089 sg_destroy_view(rt->color_att_view);
1091 if (rt->tex_view.id != SG_INVALID_ID) {
1092 sg_destroy_view(rt->tex_view);
1094 if (rt->depth_att_view.id != SG_INVALID_ID) {
1095 sg_destroy_view(rt->depth_att_view);
1097 if (rt->color_img.id != SG_INVALID_ID) {
1098 sg_destroy_image(rt->color_img);
1100 if (rt->depth_img.id != SG_INVALID_ID) {
1101 sg_destroy_image(rt->depth_img);
1103 if (rt->sampler.id != SG_INVALID_ID) {
1104 sg_destroy_sampler(rt->sampler);
1106 rt->freed = true;
1109static void rt_destructor(void *data)
1111 GfxRenderTarget *rt = (GfxRenderTarget *)data;
1112 if (rt) {
1113 rt_free_resources(rt);
1114 free(rt);
1119 * (%make-render-target width height) -> <render-target> or #f
1121 * Creates an offscreen color render target backed by an sg_image with
1122 * color_attachment usage, plus the views and sampler needed to render
1123 * into it and sample it as a texture.
1124 */
1125static Value native_make_render_target(SigilVM *vm, int argc, Value *args)
1127 if (argc < 2) {
1128 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1129 "make-render-target: requires width, height arguments");
1130 return SIGIL_UNDEFINED;
1133 int w = (int)value_to_float(args[0]);
1134 int h = (int)value_to_float(args[1]);
1135 if (w <= 0 || h <= 0) {
1136 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1137 "make-render-target: width and height must be positive");
1138 return SIGIL_FALSE;
1141 /* Pixel format and sample count default to sg_environment.defaults
1142 * (i.e., the swap chain's color format / sample count). Matching them
1143 * is required so sgp's default pipelines validate against this pass —
1144 * sgp pipelines bake in the format/sample count from sgp_setup. */
1145 sg_image_desc img_desc = {
1146 .usage = { .color_attachment = true },
1147 .width = w,
1148 .height = h,
1149 };
1150 sg_image img = sg_make_image(&img_desc);
1151 if (img.id == SG_INVALID_ID) {
1152 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1153 "make-render-target: failed to create color image");
1154 return SIGIL_FALSE;
1157 sg_view_desc att_desc = {
1158 .color_attachment = { .image = img },
1159 .label = "sigil-rt-color-attachment",
1160 };
1161 sg_view att_view = sg_make_view(&att_desc);
1162 if (att_view.id == SG_INVALID_ID) {
1163 sg_destroy_image(img);
1164 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1165 "make-render-target: failed to create color attachment view");
1166 return SIGIL_FALSE;
1169 sg_view tex_view = sgp_make_texture_view_from_image(img, "sigil-rt-texture");
1170 if (tex_view.id == SG_INVALID_ID) {
1171 sg_destroy_view(att_view);
1172 sg_destroy_image(img);
1173 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1174 "make-render-target: failed to create texture view");
1175 return SIGIL_FALSE;
1178 /* Depth-stencil attachment — needed only for pipeline-validation
1179 * compatibility with sgp's default pipelines, which bake in the
1180 * swap chain's depth-stencil format. */
1181 sg_image_desc depth_desc = {
1182 .usage = { .depth_stencil_attachment = true },
1183 .width = w,
1184 .height = h,
1185 };
1186 sg_image depth_img = sg_make_image(&depth_desc);
1187 if (depth_img.id == SG_INVALID_ID) {
1188 sg_destroy_view(tex_view);
1189 sg_destroy_view(att_view);
1190 sg_destroy_image(img);
1191 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1192 "make-render-target: failed to create depth image");
1193 return SIGIL_FALSE;
1196 sg_view_desc depth_att_desc = {
1197 .depth_stencil_attachment = { .image = depth_img },
1198 .label = "sigil-rt-depth-attachment",
1199 };
1200 sg_view depth_att_view = sg_make_view(&depth_att_desc);
1201 if (depth_att_view.id == SG_INVALID_ID) {
1202 sg_destroy_image(depth_img);
1203 sg_destroy_view(tex_view);
1204 sg_destroy_view(att_view);
1205 sg_destroy_image(img);
1206 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1207 "make-render-target: failed to create depth attachment view");
1208 return SIGIL_FALSE;
1211 sg_sampler_desc smp_desc = {
1212 .min_filter = SG_FILTER_LINEAR,
1213 .mag_filter = SG_FILTER_LINEAR,
1214 .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
1215 .wrap_v = SG_WRAP_CLAMP_TO_EDGE,
1216 };
1217 sg_sampler smp = sg_make_sampler(&smp_desc);
1219 GfxRenderTarget *rt = malloc(sizeof(GfxRenderTarget));
1220 if (!rt) {
1221 sg_destroy_sampler(smp);
1222 sg_destroy_view(depth_att_view);
1223 sg_destroy_image(depth_img);
1224 sg_destroy_view(tex_view);
1225 sg_destroy_view(att_view);
1226 sg_destroy_image(img);
1227 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
1228 "make-render-target: out of memory");
1229 return SIGIL_FALSE;
1232 rt->color_img = img;
1233 rt->color_att_view = att_view;
1234 rt->tex_view = tex_view;
1235 rt->sampler = smp;
1236 rt->depth_img = depth_img;
1237 rt->depth_att_view = depth_att_view;
1238 rt->width = w;
1239 rt->height = h;
1240 rt->freed = false;
1242 ensure_rt_type(vm);
1243 return sigil_make_foreign(vm, rt_type_tag, rt, rt_destructor,
1244 sizeof(GfxRenderTarget));
1248 * (render-target? obj) -> boolean
1249 */
1250static Value native_render_target_p(SigilVM *vm, int argc, Value *args)
1252 (void)argc;
1253 return get_rt(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
1257 * (render-target-width rt) -> integer
1258 */
1259static Value native_render_target_width(SigilVM *vm, int argc, Value *args)
1261 (void)argc;
1262 GfxRenderTarget *rt = get_rt(vm, args[0]);
1263 if (!rt) {
1264 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1265 "render-target-width: expected render-target");
1266 return SIGIL_UNDEFINED;
1268 return sigil_fixnum(rt->width);
1272 * (render-target-height rt) -> integer
1273 */
1274static Value native_render_target_height(SigilVM *vm, int argc, Value *args)
1276 (void)argc;
1277 GfxRenderTarget *rt = get_rt(vm, args[0]);
1278 if (!rt) {
1279 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1280 "render-target-height: expected render-target");
1281 return SIGIL_UNDEFINED;
1283 return sigil_fixnum(rt->height);
1287 * (render-target-free! rt) - Explicit cleanup of GPU resources
1289 * Idempotent. After this call the render target is unusable; the
1290 * destructor on GC will be a no-op.
1291 */
1292static Value native_render_target_free(SigilVM *vm, int argc, Value *args)
1294 (void)argc;
1295 GfxRenderTarget *rt = get_rt(vm, args[0]);
1296 if (!rt) {
1297 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1298 "render-target-free!: expected render-target");
1299 return SIGIL_UNDEFINED;
1301 rt_free_resources(rt);
1302 return SIGIL_NIL;
1306 * (%begin-rt-pass rt) - Push a new sokol_gp queue + sokol_gfx pass that
1307 * targets the render target. All subsequent draws land in rt's color
1308 * image until %end-rt-pass is called.
1309 */
1310static Value native_begin_rt_pass(SigilVM *vm, int argc, Value *args)
1312 (void)argc;
1313 GfxRenderTarget *rt = get_rt(vm, args[0]);
1314 if (!rt) {
1315 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1316 "%begin-rt-pass: expected render-target");
1317 return SIGIL_UNDEFINED;
1319 if (rt->freed) {
1320 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1321 "%begin-rt-pass: render target has been freed");
1322 return SIGIL_UNDEFINED;
1325 /* Push an inner sgp state on the stack with the render target's size.
1326 * sgp's state stack ensures inner sgp_flush only emits commands queued
1327 * between this sgp_begin and its matching sgp_end. */
1328 sgp_begin(rt->width, rt->height);
1329 sgp_viewport(0, 0, rt->width, rt->height);
1330 sgp_project(0, (float)rt->width, 0, (float)rt->height);
1332 /* Color: clear to transparent black on entry, keep contents on exit
1333 * (the swap-chain composite reads it). Depth: don't load, don't
1334 * store — the depth attachment exists only to satisfy sgp's
1335 * pipeline-validation requirement that pass and pipeline depth
1336 * formats match; 2D rendering never reads or writes it. */
1337 sg_pass_action act = {
1338 .colors[0] = {
1339 .load_action = SG_LOADACTION_CLEAR,
1340 .store_action = SG_STOREACTION_STORE,
1341 .clear_value = {0.0f, 0.0f, 0.0f, 0.0f},
1342 },
1343 .depth = {
1344 .load_action = SG_LOADACTION_DONTCARE,
1345 .store_action = SG_STOREACTION_DONTCARE,
1346 },
1347 };
1348 sg_pass pass = {
1349 .action = act,
1350 .attachments = {
1351 .colors[0] = rt->color_att_view,
1352 .depth_stencil = rt->depth_att_view,
1353 },
1354 };
1355 sg_begin_pass(&pass);
1357 return SIGIL_NIL;
1361 * (%end-rt-pass) - Flush queued commands to the current render target,
1362 * end the sokol_gfx pass, and pop the inner sgp state.
1364 * Must balance a prior %begin-rt-pass call.
1365 */
1366static Value native_end_rt_pass(SigilVM *vm, int argc, Value *args)
1368 (void)vm; (void)argc; (void)args;
1370 sgp_flush();
1371 sg_end_pass();
1372 sgp_end();
1374 return SIGIL_NIL;
1378 * (render-target->texture rt) -> <texture>
1380 * Returns a texture wrapper that borrows the render target's image,
1381 * texture view, and sampler. The returned texture is invalid after the
1382 * render target is freed; callers must keep the render target alive for
1383 * the lifetime of the wrapper.
1384 */
1385static Value native_render_target_to_texture(SigilVM *vm, int argc, Value *args)
1387 (void)argc;
1388 GfxRenderTarget *rt = get_rt(vm, args[0]);
1389 if (!rt) {
1390 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1391 "render-target->texture: expected render-target");
1392 return SIGIL_UNDEFINED;
1395 GfxTexture *tex = malloc(sizeof(GfxTexture));
1396 if (!tex) {
1397 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
1398 "render-target->texture: out of memory");
1399 return SIGIL_FALSE;
1402 tex->handle = rt->color_img;
1403 tex->sampler = rt->sampler;
1404 tex->view = rt->tex_view;
1405 tex->width = rt->width;
1406 tex->height = rt->height;
1407 tex->owns_resources = false;
1409 ensure_texture_type(vm);
1410 return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
1411 sizeof(GfxTexture));
1415 * (draw-render-target rt x y w h) - Draw the render target's color image
1416 * as a textured rect on the current pass.
1418 * Convenience over (draw-texture (render-target->texture rt) x y w h):
1419 * skips the texture wrapper allocation.
1420 */
1421static Value native_draw_render_target(SigilVM *vm, int argc, Value *args)
1423 if (argc < 5) {
1424 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1425 "draw-render-target: requires rt, x, y, w, h");
1426 return SIGIL_UNDEFINED;
1428 GfxRenderTarget *rt = get_rt(vm, args[0]);
1429 if (!rt) {
1430 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1431 "draw-render-target: expected render-target");
1432 return SIGIL_UNDEFINED;
1435 float x = value_to_float(args[1]);
1436 float y = value_to_float(args[2]);
1437 float w = value_to_float(args[3]);
1438 float h = value_to_float(args[4]);
1440 sgp_set_view(0, rt->tex_view);
1441 sgp_set_sampler(0, rt->sampler);
1443 /* Flip V on sample: sokol_gfx framebuffers write to texture memory in
1444 * GL bottom-up convention even on top-down backends, so a render
1445 * target sampled with the default top-down UV looks vertically
1446 * mirrored on the swap chain. Sourcing from y=height with h=-height
1447 * inverts the texcoord range so the composite is upright. */
1448 sgp_rect dest = {x, y, w, h};
1449 sgp_rect src = {0, (float)rt->height, (float)rt->width, -(float)rt->height};
1450 sgp_draw_textured_rect(0, dest, src);
1452 sgp_reset_view(0);
1453 sgp_reset_sampler(0);
1455 return SIGIL_NIL;
1458/* ============================================================
1459 * SHADERS (Phase 2 — custom fragment shaders for post-processing)
1460 * ============================================================ */
1462static void ensure_shader_type(SigilVM *vm)
1464 if (sigil_is_undefined(shader_type_tag)) {
1465 shader_type_tag = sigil_intern_symbol(vm, "sigil-graphics-shader", 21);
1469static GfxShader *get_shader(SigilVM *vm, Value v)
1471 if (!sigil_is_foreign(v)) return NULL;
1472 ensure_shader_type(vm);
1473 if (sigil_foreign_type(v) != shader_type_tag) return NULL;
1474 return (GfxShader *)sigil_foreign_data(v);
1477static void shader_free_resources(GfxShader *sh)
1479 if (!sh || sh->freed) return;
1480 if (active_shader == sh) {
1481 active_shader = NULL;
1483 if (sh->pipeline.id != SG_INVALID_ID) {
1484 sg_destroy_pipeline(sh->pipeline);
1486 if (sh->shader.id != SG_INVALID_ID) {
1487 sg_destroy_shader(sh->shader);
1489 sh->freed = true;
1492static void shader_destructor(void *data)
1494 GfxShader *sh = (GfxShader *)data;
1495 if (sh) {
1496 shader_free_resources(sh);
1497 free(sh);
1501/* Map a GLSL type token to sokol_gfx uniform type + size in bytes
1502 * (NATIVE layout — same as STD140 except for vec3, which we don't use).
1503 * Returns 0 on unrecognized type. */
1504static int map_glsl_type(const char *tok, sg_uniform_type *out_type, uint32_t *out_size)
1506 if (strcmp(tok, "float") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT; *out_size = 4; return 1; }
1507 if (strcmp(tok, "vec2") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT2; *out_size = 8; return 1; }
1508 if (strcmp(tok, "vec3") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT3; *out_size = 12; return 1; }
1509 if (strcmp(tok, "vec4") == 0) { *out_type = SG_UNIFORMTYPE_FLOAT4; *out_size = 16; return 1; }
1510 if (strcmp(tok, "mat4") == 0) { *out_type = SG_UNIFORMTYPE_MAT4; *out_size = 64; return 1; }
1511 return 0;
1514/* Parse the user's fragment GLSL for `uniform <type> <name>;` declarations.
1516 * Builds the GfxShader's uniform layout. Skips sampler2D — those are
1517 * texture bindings, not uniform-block entries (the channel-0 binding is
1518 * managed by the draw primitive). Recognised types: float, vec2, vec3,
1519 * vec4, mat4. Unknown types are silently skipped (the shader-create call
1520 * will fail later at link time if names mismatch — fine).
1522 * NATIVE layout: tightly packed, no padding. */
1523static void parse_fragment_uniforms(GfxShader *sh, const char *frag_src)
1525 const char *p = frag_src;
1526 uint32_t cursor = 0;
1527 sh->num_uniforms = 0;
1528 sh->u_time_index = -1;
1529 sh->u_resolution_index = -1;
1531 while (*p) {
1532 /* Skip whitespace, then look for the literal "uniform " token at
1533 * a line boundary or after whitespace. */
1534 const char *u = strstr(p, "uniform");
1535 if (!u) break;
1536 /* Make sure 'uniform' is at start of token (preceded by whitespace
1537 * or newline or BOF). */
1538 if (u != frag_src) {
1539 char prev = u[-1];
1540 if (prev != ' ' && prev != '\t' && prev != '\n' && prev != '\r') {
1541 p = u + 7;
1542 continue;
1545 const char *q = u + 7;
1546 while (*q == ' ' || *q == '\t') q++;
1547 /* Read type token. */
1548 char type_buf[32];
1549 size_t ti = 0;
1550 while (*q && *q != ' ' && *q != '\t' && ti < sizeof(type_buf)-1) {
1551 type_buf[ti++] = *q++;
1553 type_buf[ti] = '\0';
1554 /* Skip sampler2D / sampler types — those are texture bindings,
1555 * not uniform-block entries. */
1556 if (strncmp(type_buf, "sampler", 7) == 0) {
1557 p = q;
1558 continue;
1560 sg_uniform_type utype = SG_UNIFORMTYPE_INVALID;
1561 uint32_t usize = 0;
1562 if (!map_glsl_type(type_buf, &utype, &usize)) {
1563 p = q;
1564 continue;
1566 while (*q == ' ' || *q == '\t') q++;
1567 /* Read name token (until ';' or whitespace or '['). */
1568 char name_buf[SIGIL_GFX_UNIFORM_NAME_MAX];
1569 size_t ni = 0;
1570 while (*q && *q != ';' && *q != ' ' && *q != '\t' && *q != '[' &&
1571 ni < sizeof(name_buf)-1) {
1572 name_buf[ni++] = *q++;
1574 name_buf[ni] = '\0';
1575 if (ni == 0) { p = q; continue; }
1576 if (sh->num_uniforms >= SIGIL_GFX_MAX_UNIFORMS) break;
1577 if (cursor + usize > sizeof(sh->buffer)) break;
1578 GfxUniformEntry *ent = &sh->uniforms[sh->num_uniforms];
1579 strncpy(ent->name, name_buf, sizeof(ent->name)-1);
1580 ent->name[sizeof(ent->name)-1] = '\0';
1581 ent->type = utype;
1582 ent->offset = cursor;
1583 ent->size = usize;
1584 if (strcmp(name_buf, "u_time") == 0) {
1585 sh->u_time_index = sh->num_uniforms;
1586 } else if (strcmp(name_buf, "u_resolution") == 0) {
1587 sh->u_resolution_index = sh->num_uniforms;
1589 cursor += usize;
1590 sh->num_uniforms++;
1591 p = q;
1593 sh->buffer_size = cursor;
1594 memset(sh->buffer, 0, sizeof(sh->buffer));
1597/* (load-shader vertex-source fragment-source) -> <shader> or #f */
1598static Value native_load_shader(SigilVM *vm, int argc, Value *args)
1600 if (argc < 2) {
1601 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1602 "load-shader: requires vertex-source, fragment-source");
1603 return SIGIL_UNDEFINED;
1606 const char *vs_src = sigil_string_bytes(args[0]);
1607 const char *fs_src = sigil_string_bytes(args[1]);
1608 if (!vs_src || !fs_src) {
1609 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1610 "load-shader: expected two strings");
1611 return SIGIL_FALSE;
1614 GfxShader *sh = malloc(sizeof(GfxShader));
1615 if (!sh) {
1616 sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
1617 "load-shader: out of memory");
1618 return SIGIL_FALSE;
1620 memset(sh, 0, sizeof(*sh));
1621 sh->shader.id = SG_INVALID_ID;
1622 sh->pipeline.id = SG_INVALID_ID;
1624 parse_fragment_uniforms(sh, fs_src);
1626 sg_shader_desc desc = {0};
1627 /* Vertex attributes — must match sgp's vertex layout (location 0 =
1628 * vec4 coord, location 1 = vec4 color). The shader the user writes
1629 * must declare these inputs at the matching locations; for GL the
1630 * link uses glBindAttribLocation by glsl_name. */
1631 desc.attrs[SGP_VS_ATTR_COORD].glsl_name = "coord";
1632 desc.attrs[SGP_VS_ATTR_COLOR].glsl_name = "color";
1633 /* One sampler+view pair on the fragment stage at slot 0 — matches
1634 * sgp's default convention. The user fragment shader names this
1635 * sampler "iTexChannel0_iSmpChannel0" (sgp's canonical name). */
1636 desc.samplers[0].stage = SG_SHADERSTAGE_FRAGMENT;
1637 desc.samplers[0].sampler_type = SG_SAMPLERTYPE_FILTERING;
1638 desc.views[0].texture.stage = SG_SHADERSTAGE_FRAGMENT;
1639 desc.views[0].texture.image_type = SG_IMAGETYPE_2D;
1640 desc.views[0].texture.sample_type = SG_IMAGESAMPLETYPE_FLOAT;
1641 desc.texture_sampler_pairs[0].stage = SG_SHADERSTAGE_FRAGMENT;
1642 desc.texture_sampler_pairs[0].view_slot = 0;
1643 desc.texture_sampler_pairs[0].sampler_slot = 0;
1644 desc.texture_sampler_pairs[0].glsl_name = "iTexChannel0_iSmpChannel0";
1646 /* Uniform block on the fragment stage at slot 1 (matches sgp's
1647 * SGP_UNIFORM_SLOT_FRAGMENT — sgp_flush emits fragment uniforms
1648 * to slot 1 when sgp_set_uniform's fs_size > 0). NATIVE layout —
1649 * tightly packed, alignment=1, matches my parser's offset
1650 * computation. array_count must be >= 1 (sokol asserts > 0 in
1651 * _sg_uniform_size). */
1652 if (sh->num_uniforms > 0) {
1653 desc.uniform_blocks[1].stage = SG_SHADERSTAGE_FRAGMENT;
1654 desc.uniform_blocks[1].size = sh->buffer_size;
1655 desc.uniform_blocks[1].layout = SG_UNIFORMLAYOUT_NATIVE;
1656 for (int i = 0; i < sh->num_uniforms; i++) {
1657 desc.uniform_blocks[1].glsl_uniforms[i].type = sh->uniforms[i].type;
1658 desc.uniform_blocks[1].glsl_uniforms[i].glsl_name = sh->uniforms[i].name;
1659 desc.uniform_blocks[1].glsl_uniforms[i].array_count = 1;
1663 desc.vertex_func.entry = "main";
1664 desc.fragment_func.entry = "main";
1665 desc.vertex_func.source = vs_src;
1666 desc.fragment_func.source = fs_src;
1668 sh->shader = sg_make_shader(&desc);
1669 if (sh->shader.id == SG_INVALID_ID) {
1670 free(sh);
1671 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1672 "load-shader: shader compile/link failed (see sokol log above)");
1673 return SIGIL_FALSE;
1676 /* Build a custom sgp pipeline for this shader. Default to alpha blend
1677 * since post-processing typically overlays a textured rect on the
1678 * swap chain. */
1679 sgp_pipeline_desc pip_desc = {0};
1680 pip_desc.shader = sh->shader;
1681 pip_desc.blend_mode = SGP_BLENDMODE_BLEND;
1682 pip_desc.has_vs_color = true; /* sgp's vertex layout always has color */
1683 sh->pipeline = sgp_make_pipeline(&pip_desc);
1684 if (sh->pipeline.id == SG_INVALID_ID) {
1685 sg_destroy_shader(sh->shader);
1686 free(sh);
1687 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1688 "load-shader: pipeline create failed");
1689 return SIGIL_FALSE;
1692 ensure_shader_type(vm);
1693 return sigil_make_foreign(vm, shader_type_tag, sh, shader_destructor,
1694 sizeof(GfxShader));
1697/* (shader? obj) -> boolean */
1698static Value native_shader_p(SigilVM *vm, int argc, Value *args)
1700 (void)argc;
1701 return get_shader(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
1704/* (shader-free! shader) — explicit cleanup. Idempotent. */
1705static Value native_shader_free(SigilVM *vm, int argc, Value *args)
1707 (void)argc;
1708 GfxShader *sh = get_shader(vm, args[0]);
1709 if (!sh) {
1710 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1711 "shader-free!: expected shader");
1712 return SIGIL_UNDEFINED;
1714 shader_free_resources(sh);
1715 return SIGIL_NIL;
1718/* Find a uniform by name in the shader's layout. Returns -1 if absent. */
1719static int shader_uniform_index(const GfxShader *sh, const char *name)
1721 for (int i = 0; i < sh->num_uniforms; i++) {
1722 if (strcmp(sh->uniforms[i].name, name) == 0) return i;
1724 return -1;
1727/* Refresh u_time + u_resolution into the active shader's buffer and
1728 * push the buffer to the fragment stage uniform block. Called from
1729 * draw primitives that use the active shader. */
1730static void apply_active_shader_uniforms(void)
1732 GfxShader *sh = active_shader;
1733 if (!sh || sh->freed) return;
1734 if (sh->u_time_index >= 0) {
1735 float t = gfx_elapsed_seconds();
1736 memcpy(sh->buffer + sh->uniforms[sh->u_time_index].offset,
1737 &t, sizeof(t));
1739 if (sh->u_resolution_index >= 0) {
1740 float res[2] = {(float)sig_gfx_width(), (float)sig_gfx_height()};
1741 if (virtual_viewport_enabled) {
1742 res[0] = (float)virtual_width;
1743 res[1] = (float)virtual_height;
1745 memcpy(sh->buffer + sh->uniforms[sh->u_resolution_index].offset,
1746 res, sizeof(res));
1748 if (sh->buffer_size > 0) {
1749 sgp_set_uniform(NULL, 0, sh->buffer, sh->buffer_size);
1753/* (set-shader-uniform shader name value) — write a value into the
1754 * shader's uniform buffer at the offset matching `name`.
1756 * Accepts:
1757 * number → float
1758 * list of 2 → vec2
1759 * list of 3 → vec3
1760 * list of 4 → vec4
1761 * 16 floats → mat4 (rare; pass as a list)
1763 * sampler2D uniforms are NOT set through this path; channel 0 is bound
1764 * by the draw primitive (e.g., draw-render-target) and additional
1765 * samplers are out of scope for Phase 2. */
1766static Value native_set_shader_uniform(SigilVM *vm, int argc, Value *args)
1768 if (argc < 3) {
1769 sigil__vm_error(vm, SIGIL_ERR_ARITY,
1770 "set-shader-uniform: requires shader, name, value");
1771 return SIGIL_UNDEFINED;
1773 GfxShader *sh = get_shader(vm, args[0]);
1774 if (!sh) {
1775 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1776 "set-shader-uniform: expected shader");
1777 return SIGIL_UNDEFINED;
1779 const char *name = NULL;
1780 if (sigil_is_symbol(args[1])) {
1781 name = sigil_symbol_name(args[1]);
1782 } else if (sigil_is_string(args[1])) {
1783 name = sigil_string_bytes(args[1]);
1785 if (!name) {
1786 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1787 "set-shader-uniform: name must be symbol or string");
1788 return SIGIL_UNDEFINED;
1790 int idx = shader_uniform_index(sh, name);
1791 if (idx < 0) {
1792 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1793 "set-shader-uniform: no such uniform in shader");
1794 return SIGIL_UNDEFINED;
1796 GfxUniformEntry *ent = &sh->uniforms[idx];
1797 Value v = args[2];
1798 /* Number → float. */
1799 if (sigil_is_fixnum(v) || sigil_is_flonum(v)) {
1800 if (ent->type != SG_UNIFORMTYPE_FLOAT) {
1801 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1802 "set-shader-uniform: type mismatch (got number, uniform is not float)");
1803 return SIGIL_UNDEFINED;
1805 float f = value_to_float(v);
1806 memcpy(sh->buffer + ent->offset, &f, sizeof(f));
1807 } else if (sigil_is_pair(v) || sigil_is_null(v)) {
1808 /* List of numbers. */
1809 float scratch[16];
1810 int n = 0;
1811 Value cur = v;
1812 while (sigil_is_pair(cur) && n < 16) {
1813 scratch[n++] = value_to_float(sigil_car(cur));
1814 cur = sigil_cdr(cur);
1816 uint32_t needed = ent->size / sizeof(float);
1817 if ((uint32_t)n != needed) {
1818 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1819 "set-shader-uniform: list length doesn't match uniform size");
1820 return SIGIL_UNDEFINED;
1822 memcpy(sh->buffer + ent->offset, scratch, ent->size);
1823 } else {
1824 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1825 "set-shader-uniform: value must be number or list of numbers");
1826 return SIGIL_UNDEFINED;
1828 /* If this shader is currently active, push the updated buffer right
1829 * away so the new value lands in the next draw without waiting for
1830 * the next auto-refresh. */
1831 if (active_shader == sh && sh->buffer_size > 0) {
1832 sgp_set_uniform(NULL, 0, sh->buffer, sh->buffer_size);
1834 return SIGIL_NIL;
1837/* (%bind-shader shader) — set as active sgp pipeline + uniform source. */
1838static Value native_bind_shader(SigilVM *vm, int argc, Value *args)
1840 (void)argc;
1841 GfxShader *sh = get_shader(vm, args[0]);
1842 if (!sh) {
1843 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
1844 "%bind-shader: expected shader");
1845 return SIGIL_UNDEFINED;
1847 if (sh->freed) {
1848 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
1849 "%bind-shader: shader has been freed");
1850 return SIGIL_UNDEFINED;
1852 sgp_set_pipeline(sh->pipeline);
1853 active_shader = sh;
1854 apply_active_shader_uniforms();
1855 return SIGIL_NIL;
1858/* (%unbind-shader) — restore default sgp pipeline + clear active shader.
1859 * sgp_reset_pipeline internally calls sgp_set_pipeline(INVALID), which
1860 * memsets the uniform buffer; calling sgp_reset_uniform afterwards
1861 * triggers an "invalid pipeline" assertion, so don't. */
1862static Value native_unbind_shader(SigilVM *vm, int argc, Value *args)
1864 (void)vm; (void)argc; (void)args;
1865 sgp_reset_pipeline();
1866 active_shader = NULL;
1867 return SIGIL_NIL;
1870/* ============================================================
1871 * MODULE INITIALIZATION
1872 * ============================================================ */
1874/* Forward declarations for related modules */
1875extern void sigil__init_sigil_graphics_image_module(SigilVM *vm);
1876extern void sigil__init_sigil_graphics_font_module(SigilVM *vm);
1878void sigil__init_sigil_graphics_module(SigilVM *vm)
1880 SigilModule *module = sigil_begin_module(vm, "(sigil graphics)");
1881 if (!module) return;
1883 /* Setup/shutdown */
1884 sigil_module_register_native(vm, "gfx-setup", native_gfx_setup,
1885 SIGIL_ARITY_EXACT(0), "Initialize graphics");
1886 sigil_module_register_native(vm, "gfx-shutdown", native_gfx_shutdown,
1887 SIGIL_ARITY_EXACT(0), "Shutdown graphics");
1888 sigil_module_register_native(vm, "gfx-initialized?", native_gfx_initialized,
1889 SIGIL_ARITY_EXACT(0), "Is graphics initialized?");
1891 /* Viewport */
1892 sigil_module_register_native(vm, "set-viewport", native_set_viewport,
1893 SIGIL_ARITY_RANGE(1, 2), "Set virtual viewport with letterboxing");
1894 sigil_module_register_native(vm, "set-letterbox-color", native_set_letterbox_color,
1895 SIGIL_ARITY_RANGE(3, 4), "Set letterbox bar color");
1897 /* Frame management */
1898 sigil_module_register_native(vm, "begin-frame", native_begin_frame,
1899 SIGIL_ARITY_EXACT(0), "Begin a new frame");
1900 sigil_module_register_native(vm, "end-frame", native_end_frame,
1901 SIGIL_ARITY_EXACT(0), "End current frame");
1903 /* Drawing */
1904 sigil_module_register_native(vm, "clear-screen", native_clear_screen,
1905 SIGIL_ARITY_RANGE(3, 4), "Set screen clear color");
1906 sigil_module_register_native(vm, "set-color", native_set_color,
1907 SIGIL_ARITY_RANGE(3, 4), "Set draw color");
1908 sigil_module_register_native(vm, "draw-filled-rect", native_draw_filled_rect,
1909 SIGIL_ARITY_EXACT(4), "Draw filled rectangle");
1910 sigil_module_register_native(vm, "draw-rect", native_draw_rect,
1911 SIGIL_ARITY_EXACT(4), "Draw rectangle outline");
1912 sigil_module_register_native(vm, "draw-line", native_draw_line,
1913 SIGIL_ARITY_EXACT(4), "Draw a line");
1914 sigil_module_register_native(vm, "draw-point", native_draw_point,
1915 SIGIL_ARITY_EXACT(2), "Draw a point");
1916 sigil_module_register_native(vm, "draw-triangle", native_draw_triangle,
1917 SIGIL_ARITY_EXACT(6), "Draw triangle outline");
1918 sigil_module_register_native(vm, "fill-triangle", native_fill_triangle,
1919 SIGIL_ARITY_EXACT(6), "Draw filled triangle");
1920 sigil_module_register_native(vm, "draw-filled-circle", native_draw_filled_circle,
1921 SIGIL_ARITY_RANGE(3, 4), "Draw filled circle");
1923 /* Blend mode (raw C primitives — wrapped by Sigil set-blend-mode for
1924 * tracking; users should prefer the Sigil wrapper or with-blend-mode.) */
1925 sigil_module_register_native(vm, "%set-blend-mode-native", native_set_blend_mode,
1926 SIGIL_ARITY_EXACT(1), "Set blend mode ('normal, 'additive, 'none)");
1927 sigil_module_register_native(vm, "%reset-blend-mode-native", native_reset_blend_mode,
1928 SIGIL_ARITY_EXACT(0), "Reset blend mode to sokol_gp default");
1930 /* Transform stack */
1931 sigil_module_register_native(vm, "push-transform", native_push_transform,
1932 SIGIL_ARITY_EXACT(0), "Save transform state");
1933 sigil_module_register_native(vm, "pop-transform", native_pop_transform,
1934 SIGIL_ARITY_EXACT(0), "Restore transform state");
1935 sigil_module_register_native(vm, "reset-transform", native_reset_transform,
1936 SIGIL_ARITY_EXACT(0), "Reset to identity transform");
1937 sigil_module_register_native(vm, "translate", native_translate,
1938 SIGIL_ARITY_EXACT(2), "Translate by (x, y)");
1939 sigil_module_register_native(vm, "rotate", native_rotate,
1940 SIGIL_ARITY_EXACT(1), "Rotate by angle (radians)");
1941 sigil_module_register_native(vm, "rotate-at", native_rotate_at,
1942 SIGIL_ARITY_EXACT(3), "Rotate around point");
1943 sigil_module_register_native(vm, "scale", native_scale,
1944 SIGIL_ARITY_EXACT(2), "Scale by (sx, sy)");
1945 sigil_module_register_native(vm, "scale-at", native_scale_at,
1946 SIGIL_ARITY_EXACT(4), "Scale around point");
1948 /* Texture functions */
1949 sigil_module_register_native(vm, "load-texture", native_load_texture,
1950 SIGIL_ARITY_EXACT(1), "Create GPU texture from image");
1951 sigil_module_register_native(vm, "texture?", native_texture_p,
1952 SIGIL_ARITY_EXACT(1), "Check if object is a texture");
1953 sigil_module_register_native(vm, "texture-width", native_texture_width,
1954 SIGIL_ARITY_EXACT(1), "Get texture width");
1955 sigil_module_register_native(vm, "texture-height", native_texture_height,
1956 SIGIL_ARITY_EXACT(1), "Get texture height");
1957 sigil_module_register_native(vm, "draw-texture", native_draw_texture,
1958 SIGIL_ARITY_RANGE(3, 5), "Draw texture at position");
1959 sigil_module_register_native(vm, "draw-texture-region", native_draw_texture_region,
1960 SIGIL_ARITY_EXACT(9), "Draw texture region");
1962 /* Render targets — %begin-rt-pass / %end-rt-pass are raw primitives
1963 * wrapped by the with-render-target macro for stack discipline. */
1964 sigil_module_register_native(vm, "%make-render-target", native_make_render_target,
1965 SIGIL_ARITY_EXACT(2), "Create offscreen render target");
1966 sigil_module_register_native(vm, "render-target?", native_render_target_p,
1967 SIGIL_ARITY_EXACT(1), "Check if object is a render target");
1968 sigil_module_register_native(vm, "render-target-width", native_render_target_width,
1969 SIGIL_ARITY_EXACT(1), "Get render target width");
1970 sigil_module_register_native(vm, "render-target-height", native_render_target_height,
1971 SIGIL_ARITY_EXACT(1), "Get render target height");
1972 sigil_module_register_native(vm, "render-target-free!", native_render_target_free,
1973 SIGIL_ARITY_EXACT(1), "Free render target GPU resources");
1974 sigil_module_register_native(vm, "%begin-rt-pass", native_begin_rt_pass,
1975 SIGIL_ARITY_EXACT(1), "Begin offscreen render pass");
1976 sigil_module_register_native(vm, "%end-rt-pass", native_end_rt_pass,
1977 SIGIL_ARITY_EXACT(0), "End offscreen render pass");
1978 sigil_module_register_native(vm, "render-target->texture", native_render_target_to_texture,
1979 SIGIL_ARITY_EXACT(1), "Borrow render target as texture");
1980 sigil_module_register_native(vm, "draw-render-target", native_draw_render_target,
1981 SIGIL_ARITY_EXACT(5), "Draw render target as textured quad");
1983 /* Shaders — custom fragment shaders for post-processing.
1984 * %bind-shader / %unbind-shader are raw primitives wrapped by the
1985 * with-shader macro for stack discipline + auto-uniform refresh. */
1986 sigil_module_register_native(vm, "load-shader", native_load_shader,
1987 SIGIL_ARITY_EXACT(2), "Compile + link a shader from vertex+fragment GLSL");
1988 sigil_module_register_native(vm, "shader?", native_shader_p,
1989 SIGIL_ARITY_EXACT(1), "Check if object is a shader");
1990 sigil_module_register_native(vm, "shader-free!", native_shader_free,
1991 SIGIL_ARITY_EXACT(1), "Free shader GPU resources");
1992 sigil_module_register_native(vm, "set-shader-uniform", native_set_shader_uniform,
1993 SIGIL_ARITY_EXACT(3), "Set a uniform value on a shader");
1994 sigil_module_register_native(vm, "%bind-shader", native_bind_shader,
1995 SIGIL_ARITY_EXACT(1), "Bind a shader as the active pipeline");
1996 sigil_module_register_native(vm, "%unbind-shader", native_unbind_shader,
1997 SIGIL_ARITY_EXACT(0), "Unbind active shader, restore default");
1999 /* Export all */
2000 sigil_module_export(vm, "gfx-setup");
2001 sigil_module_export(vm, "gfx-shutdown");
2002 sigil_module_export(vm, "gfx-initialized?");
2003 sigil_module_export(vm, "set-viewport");
2004 sigil_module_export(vm, "set-letterbox-color");
2005 sigil_module_export(vm, "begin-frame");
2006 sigil_module_export(vm, "end-frame");
2007 sigil_module_export(vm, "clear-screen");
2008 sigil_module_export(vm, "set-color");
2009 sigil_module_export(vm, "draw-filled-rect");
2010 sigil_module_export(vm, "draw-rect");
2011 sigil_module_export(vm, "draw-line");
2012 sigil_module_export(vm, "draw-point");
2013 sigil_module_export(vm, "draw-triangle");
2014 sigil_module_export(vm, "fill-triangle");
2015 sigil_module_export(vm, "draw-filled-circle");
2016 sigil_module_export(vm, "%set-blend-mode-native");
2017 sigil_module_export(vm, "%reset-blend-mode-native");
2018 sigil_module_export(vm, "push-transform");
2019 sigil_module_export(vm, "pop-transform");
2020 sigil_module_export(vm, "reset-transform");
2021 sigil_module_export(vm, "translate");
2022 sigil_module_export(vm, "rotate");
2023 sigil_module_export(vm, "rotate-at");
2024 sigil_module_export(vm, "scale");
2025 sigil_module_export(vm, "scale-at");
2026 sigil_module_export(vm, "load-texture");
2027 sigil_module_export(vm, "texture?");
2028 sigil_module_export(vm, "texture-width");
2029 sigil_module_export(vm, "texture-height");
2030 sigil_module_export(vm, "draw-texture");
2031 sigil_module_export(vm, "draw-texture-region");
2032 sigil_module_export(vm, "%make-render-target");
2033 sigil_module_export(vm, "render-target?");
2034 sigil_module_export(vm, "render-target-width");
2035 sigil_module_export(vm, "render-target-height");
2036 sigil_module_export(vm, "render-target-free!");
2037 sigil_module_export(vm, "%begin-rt-pass");
2038 sigil_module_export(vm, "%end-rt-pass");
2039 sigil_module_export(vm, "render-target->texture");
2040 sigil_module_export(vm, "draw-render-target");
2041 sigil_module_export(vm, "load-shader");
2042 sigil_module_export(vm, "shader?");
2043 sigil_module_export(vm, "shader-free!");
2044 sigil_module_export(vm, "set-shader-uniform");
2045 sigil_module_export(vm, "%bind-shader");
2046 sigil_module_export(vm, "%unbind-shader");
2048 sigil_end_module(vm);
2050 /* Initialize related modules */
2051 sigil__init_sigil_graphics_image_module(vm);
2052 sigil__init_sigil_graphics_font_module(vm);