AtlatestRepositorysigil-graphics
sigil-graphics / tree / src / cgraphics.c
1
/*2
* graphics.c - Sigil Graphics Module3
*4
* Wraps sokol_gfx.h to provide 2D/3D rendering capabilities.5
*/7
#include "sigil-internal.h"9
/* Sokol headers (implementation is in sokol-graphics.c).10
*11
* On web (wasm32-wasi) there is no windowing system: sokol_app / sokol_glue are12
* replaced by the sigil-wasm-gles3 JS bridge (canvas + WebGL2 context +13
* swapchain). Those two headers are therefore excluded on the web build; every14
* 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
#endif19
#include "sokol_gfx.h"20
#if !defined(__wasm__)21
#include "sokol_glue.h"22
#endif23
#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 this36
* file. Native expands to the exact sapp / sglue calls used before, so the37
* native build is behaviour-identical. Web supplies the default framebuffer38
* swapchain manually (lifted from the Milestone-1 wasm-sokol-sprite example) and39
* 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")))42
extern int sigil_wasm_gles3_canvas_width(void);43
__attribute__((import_module("sigil_wasm_gles3"), import_name("canvas_height")))44
extern int sigil_wasm_gles3_canvas_height(void);46
static int sig_gfx_width(void) { return sigil_wasm_gles3_canvas_width(); }47
static int sig_gfx_height(void) { return sigil_wasm_gles3_canvas_height(); }49
static 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;55
}56
static 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;65
}66
#else67
static inline int sig_gfx_width(void) { return sapp_width(); }68
static inline int sig_gfx_height(void) { return sapp_height(); }69
static inline sg_environment sig_gfx_environment(void) { return sglue_environment(); }70
static inline sg_swapchain sig_gfx_swapchain(void) { return sglue_swapchain(); }71
#endif73
#ifndef M_PI74
#define M_PI 3.1415926535897932384675
#endif77
/* Maximum segments for a circle triangle fan. Stack-allocated buffer is sized78
* to this. Higher values produce smoother circles at the cost of more triangles79
* per draw call. 64 is plenty for typical bullet/UI usage. */80
#define SIGIL_GFX_CIRCLE_MAX_SEGMENTS 12882
/* External: get pixel data from image (defined in image.c) */83
extern unsigned char *sigil_graphics_image_pixels(SigilVM *vm, Value img_val, int *width, int *height);85
/* Graphics initialized flag */86
static bool gfx_initialized = false;88
/* Texture type tag (initialized at module init) */89
static Value texture_type_tag = SIGIL_UNDEFINED;91
/* Render target type tag (initialized at module init) */92
static Value rt_type_tag = SIGIL_UNDEFINED;94
/* Shader type tag (initialized at module init) */95
static Value shader_type_tag = SIGIL_UNDEFINED;97
/* Texture structure.98
*99
* `owns_resources` is false for textures that borrow their handles from100
* 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. */103
typedef 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 a116
* depth-stencil image + view. The depth-stencil attachment is only117
* needed because sgp's default pipelines bake in the swap chain's118
* depth-stencil format; offscreen passes must provide a matching119
* attachment so pipeline validation passes. We don't actually use the120
* depth buffer for 2D rendering. */121
typedef 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's134
* 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 16137
#define SIGIL_GFX_UNIFORM_BUFFER_SIZE 256 /* matches SGP_UNIFORM_CONTENT_SLOTS=64 floats */138
#define SIGIL_GFX_UNIFORM_NAME_MAX 64140
typedef 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 CPU151
* buffer that mirrors the GPU uniform block. Auto-uniforms u_time and152
* 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). */157
typedef 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 path171
* during draw-render-target / draw-texture so u_time advances and172
* u_resolution tracks viewport size without caller intervention. */173
static GfxShader *active_shader = NULL;175
/* Process start time for u_time. Set on first sg_setup. */176
static double gfx_start_time = 0.0;178
/* Current draw color */179
static float draw_color[4] = {1.0f, 1.0f, 1.0f, 1.0f};181
/* Clear color (set by clear, used in end-frame) */182
static float clear_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};184
/* SGP initialized flag */185
static bool sgp_initialized = false;187
/* Virtual viewport state */188
static bool virtual_viewport_enabled = false;189
static int virtual_width = 0;190
static int virtual_height = 0;192
/* Letterbox color (bars outside viewport) */193
static float letterbox_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};195
/* Helper to extract float from fixnum or flonum */196
static float value_to_float(Value v)197
{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;204
}206
/* Monotonic seconds since gfx-setup. Used by the shader auto-uniform207
* u_time. clock_gettime(CLOCK_MONOTONIC) is unaffected by wall-clock208
* jumps and has nanosecond resolution. */209
static float gfx_elapsed_seconds(void)210
{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);215
}217
/* ============================================================218
* NATIVE FUNCTIONS219
* ============================================================ */221
/*222
* (gfx-setup) - Initialize graphics subsystem223
* Must be called in the app-run init callback after window is created.224
*/225
static Value native_gfx_setup(SigilVM *vm, int argc, Value *args)226
{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 failures234
* 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;260
}262
/*263
* (gfx-shutdown) - Shutdown graphics subsystem264
*/265
static Value native_gfx_shutdown(SigilVM *vm, int argc, Value *args)266
{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;279
}281
/*282
* (set-letterbox-color r g b [a]) - Set the color for letterbox bars283
*284
* Default is black. Only visible when using a virtual viewport.285
*/286
static Value native_set_letterbox_color(SigilVM *vm, int argc, Value *args)287
{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;299
}301
/*302
* (set-viewport width height) - Set virtual viewport with letterboxing303
*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
*/308
static Value native_set_viewport(SigilVM *vm, int argc, Value *args)309
{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;328
}330
/*331
* (begin-frame) - Begin a new frame332
*/333
static Value native_begin_frame(SigilVM *vm, int argc, Value *args)334
{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;366
}368
/*369
* (end-frame) - End the current frame370
*/371
static Value native_end_frame(SigilVM *vm, int argc, Value *args)372
{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;397
}399
/*400
* (clear-screen r g b [a]) - Clear the viewport with a color401
*402
* When using a virtual viewport, this fills the viewport area.403
* The letterbox bars remain the pass clear color (black).404
*/405
static Value native_clear_screen(SigilVM *vm, int argc, Value *args)406
{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;429
}431
/*432
* (set-color r g b [a]) - Set current draw color433
*/434
static Value native_set_color(SigilVM *vm, int argc, Value *args)435
{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;450
}452
/*453
* (draw-filled-rect x y w h) - Draw a filled rectangle454
*/455
static Value native_draw_filled_rect(SigilVM *vm, int argc, Value *args)456
{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;470
}472
/*473
* (draw-rect x y w h) - Draw a rectangle outline474
*/475
static Value native_draw_rect(SigilVM *vm, int argc, Value *args)476
{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;497
}499
/*500
* (draw-line x1 y1 x2 y2) - Draw a line501
*/502
static Value native_draw_line(SigilVM *vm, int argc, Value *args)503
{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;518
}520
/*521
* (draw-point x y) - Draw a single point522
*/523
static Value native_draw_point(SigilVM *vm, int argc, Value *args)524
{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;537
}539
/*540
* (draw-triangle x1 y1 x2 y2 x3 y3) - Draw a triangle outline541
*/542
static Value native_draw_triangle(SigilVM *vm, int argc, Value *args)543
{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;564
}566
/*567
* (fill-triangle x1 y1 x2 y2 x3 y3) - Draw a filled triangle568
*/569
static Value native_fill_triangle(SigilVM *vm, int argc, Value *args)570
{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;587
}589
/*590
* (draw-filled-circle x y radius [segments]) - Draw a filled circle591
*592
* Renders the circle as a triangle fan around (x, y). `segments` controls593
* 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 to595
* [3, SIGIL_GFX_CIRCLE_MAX_SEGMENTS].596
*/597
static Value native_draw_filled_circle(SigilVM *vm, int argc, Value *args)598
{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;640
}642
/* ============================================================643
* BLEND MODES644
* ============================================================ */646
/* Map a Sigil symbol value to an sgp_blend_mode. Returns -1 if unknown. */647
static int blend_mode_from_value(Value v)648
{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;658
}660
/*661
* (set-blend-mode mode) - Set the current blend mode662
*663
* mode is one of: 'normal (alpha blend), 'additive, 'none.664
* Stays in effect until changed or reset-blend-mode is called.665
*/666
static Value native_set_blend_mode(SigilVM *vm, int argc, Value *args)667
{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;681
}683
/*684
* (reset-blend-mode) - Reset blend mode to sokol_gp default (no blending)685
*/686
static Value native_reset_blend_mode(SigilVM *vm, int argc, Value *args)687
{688
(void)vm; (void)argc; (void)args;689
sgp_reset_blend_mode();690
return SIGIL_NIL;691
}693
/* ============================================================694
* TRANSFORM STACK695
* ============================================================ */697
/*698
* (push-transform) - Save current transform state699
*/700
static Value native_push_transform(SigilVM *vm, int argc, Value *args)701
{702
(void)vm; (void)argc; (void)args;703
sgp_push_transform();704
return SIGIL_NIL;705
}707
/*708
* (pop-transform) - Restore previous transform state709
*/710
static Value native_pop_transform(SigilVM *vm, int argc, Value *args)711
{712
(void)vm; (void)argc; (void)args;713
sgp_pop_transform();714
return SIGIL_NIL;715
}717
/*718
* (reset-transform) - Reset to identity transform719
*/720
static Value native_reset_transform(SigilVM *vm, int argc, Value *args)721
{722
(void)vm; (void)argc; (void)args;723
sgp_reset_transform();724
return SIGIL_NIL;725
}727
/*728
* (translate x y) - Translate by (x, y)729
*/730
static Value native_translate(SigilVM *vm, int argc, Value *args)731
{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;743
}745
/*746
* (rotate angle) - Rotate by angle (in radians)747
*/748
static Value native_rotate(SigilVM *vm, int argc, Value *args)749
{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;759
}761
/*762
* (rotate-at angle x y) - Rotate around point (x, y)763
*/764
static Value native_rotate_at(SigilVM *vm, int argc, Value *args)765
{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;778
}780
/*781
* (scale sx sy) - Scale by (sx, sy)782
*/783
static Value native_scale(SigilVM *vm, int argc, Value *args)784
{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;796
}798
/*799
* (scale-at sx sy x y) - Scale around point (x, y)800
*/801
static Value native_scale_at(SigilVM *vm, int argc, Value *args)802
{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;816
}818
/*819
* (gfx-initialized?) -> boolean820
*/821
static Value native_gfx_initialized(SigilVM *vm, int argc, Value *args)822
{823
(void)vm; (void)argc; (void)args;824
return gfx_initialized ? SIGIL_TRUE : SIGIL_FALSE;825
}827
/* ============================================================828
* TEXTURE FUNCTIONS829
* ============================================================ */831
/* Initialize texture type tag */832
static void ensure_texture_type(SigilVM *vm)833
{834
if (sigil_is_undefined(texture_type_tag)) {835
texture_type_tag = sigil_intern_symbol(vm, "sigil-graphics-texture", 22);836
}837
}839
/* Get texture from Value, returns NULL if not a texture */840
static GfxTexture *get_texture(SigilVM *vm, Value v)841
{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);846
}848
/* Texture destructor */849
static void texture_destructor(void *data)850
{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
}866
}868
/*869
* (load-texture image) -> <texture> or #f870
*871
* Create a GPU texture from a CPU-side image.872
*/873
static Value native_load_texture(SigilVM *vm, int argc, Value *args)874
{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_EDGE910
};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));943
}945
/*946
* (texture? obj) -> boolean947
*/948
static Value native_texture_p(SigilVM *vm, int argc, Value *args)949
{950
(void)argc;951
return get_texture(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;952
}954
/*955
* (texture-width tex) -> integer956
*/957
static Value native_texture_width(SigilVM *vm, int argc, Value *args)958
{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);966
}968
/*969
* (texture-height tex) -> integer970
*/971
static Value native_texture_height(SigilVM *vm, int argc, Value *args)972
{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);980
}982
/*983
* (draw-texture tex x y [w h]) - Draw texture at position984
*985
* If w/h are not provided, uses texture's native size.986
*/987
static Value native_draw_texture(SigilVM *vm, int argc, Value *args)988
{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;1019
}1021
/*1022
* (draw-texture-region tex x y w h sx sy sw sh) - Draw portion of texture1023
*1024
* Draws source region (sx, sy, sw, sh) from texture to destination (x, y, w, h).1025
*/1026
static Value native_draw_texture_region(SigilVM *vm, int argc, Value *args)1027
{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;1032
}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;1038
}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;1063
}1065
/* ============================================================1066
* RENDER TARGETS1067
* ============================================================ */1069
static void ensure_rt_type(SigilVM *vm)1070
{1071
if (sigil_is_undefined(rt_type_tag)) {1072
rt_type_tag = sigil_intern_symbol(vm, "sigil-graphics-rt", 17);1073
}1074
}1076
static GfxRenderTarget *get_rt(SigilVM *vm, Value v)1077
{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);1082
}1084
/* Free a render target's GPU resources. Idempotent. */1085
static void rt_free_resources(GfxRenderTarget *rt)1086
{1087
if (!rt || rt->freed) return;1088
if (rt->color_att_view.id != SG_INVALID_ID) {1089
sg_destroy_view(rt->color_att_view);1090
}1091
if (rt->tex_view.id != SG_INVALID_ID) {1092
sg_destroy_view(rt->tex_view);1093
}1094
if (rt->depth_att_view.id != SG_INVALID_ID) {1095
sg_destroy_view(rt->depth_att_view);1096
}1097
if (rt->color_img.id != SG_INVALID_ID) {1098
sg_destroy_image(rt->color_img);1099
}1100
if (rt->depth_img.id != SG_INVALID_ID) {1101
sg_destroy_image(rt->depth_img);1102
}1103
if (rt->sampler.id != SG_INVALID_ID) {1104
sg_destroy_sampler(rt->sampler);1105
}1106
rt->freed = true;1107
}1109
static void rt_destructor(void *data)1110
{1111
GfxRenderTarget *rt = (GfxRenderTarget *)data;1112
if (rt) {1113
rt_free_resources(rt);1114
free(rt);1115
}1116
}1118
/*1119
* (%make-render-target width height) -> <render-target> or #f1120
*1121
* Creates an offscreen color render target backed by an sg_image with1122
* color_attachment usage, plus the views and sampler needed to render1123
* into it and sample it as a texture.1124
*/1125
static Value native_make_render_target(SigilVM *vm, int argc, Value *args)1126
{1127
if (argc < 2) {1128
sigil__vm_error(vm, SIGIL_ERR_ARITY,1129
"make-render-target: requires width, height arguments");1130
return SIGIL_UNDEFINED;1131
}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;1139
}1141
/* Pixel format and sample count default to sg_environment.defaults1142
* (i.e., the swap chain's color format / sample count). Matching them1143
* 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;1155
}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;1167
}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;1176
}1178
/* Depth-stencil attachment — needed only for pipeline-validation1179
* compatibility with sgp's default pipelines, which bake in the1180
* 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;1194
}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;1209
}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;1230
}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));1245
}1247
/*1248
* (render-target? obj) -> boolean1249
*/1250
static Value native_render_target_p(SigilVM *vm, int argc, Value *args)1251
{1252
(void)argc;1253
return get_rt(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;1254
}1256
/*1257
* (render-target-width rt) -> integer1258
*/1259
static Value native_render_target_width(SigilVM *vm, int argc, Value *args)1260
{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;1267
}1268
return sigil_fixnum(rt->width);1269
}1271
/*1272
* (render-target-height rt) -> integer1273
*/1274
static Value native_render_target_height(SigilVM *vm, int argc, Value *args)1275
{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;1282
}1283
return sigil_fixnum(rt->height);1284
}1286
/*1287
* (render-target-free! rt) - Explicit cleanup of GPU resources1288
*1289
* Idempotent. After this call the render target is unusable; the1290
* destructor on GC will be a no-op.1291
*/1292
static Value native_render_target_free(SigilVM *vm, int argc, Value *args)1293
{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;1300
}1301
rt_free_resources(rt);1302
return SIGIL_NIL;1303
}1305
/*1306
* (%begin-rt-pass rt) - Push a new sokol_gp queue + sokol_gfx pass that1307
* targets the render target. All subsequent draws land in rt's color1308
* image until %end-rt-pass is called.1309
*/1310
static Value native_begin_rt_pass(SigilVM *vm, int argc, Value *args)1311
{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;1318
}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;1323
}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 queued1327
* 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 exit1333
* (the swap-chain composite reads it). Depth: don't load, don't1334
* store — the depth attachment exists only to satisfy sgp's1335
* pipeline-validation requirement that pass and pipeline depth1336
* 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;1358
}1360
/*1361
* (%end-rt-pass) - Flush queued commands to the current render target,1362
* end the sokol_gfx pass, and pop the inner sgp state.1363
*1364
* Must balance a prior %begin-rt-pass call.1365
*/1366
static Value native_end_rt_pass(SigilVM *vm, int argc, Value *args)1367
{1368
(void)vm; (void)argc; (void)args;1370
sgp_flush();1371
sg_end_pass();1372
sgp_end();1374
return SIGIL_NIL;1375
}1377
/*1378
* (render-target->texture rt) -> <texture>1379
*1380
* Returns a texture wrapper that borrows the render target's image,1381
* texture view, and sampler. The returned texture is invalid after the1382
* render target is freed; callers must keep the render target alive for1383
* the lifetime of the wrapper.1384
*/1385
static Value native_render_target_to_texture(SigilVM *vm, int argc, Value *args)1386
{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;1393
}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;1400
}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));1412
}1414
/*1415
* (draw-render-target rt x y w h) - Draw the render target's color image1416
* as a textured rect on the current pass.1417
*1418
* Convenience over (draw-texture (render-target->texture rt) x y w h):1419
* skips the texture wrapper allocation.1420
*/1421
static Value native_draw_render_target(SigilVM *vm, int argc, Value *args)1422
{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;1427
}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;1433
}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 in1444
* GL bottom-up convention even on top-down backends, so a render1445
* target sampled with the default top-down UV looks vertically1446
* mirrored on the swap chain. Sourcing from y=height with h=-height1447
* 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;1456
}1458
/* ============================================================1459
* SHADERS (Phase 2 — custom fragment shaders for post-processing)1460
* ============================================================ */1462
static void ensure_shader_type(SigilVM *vm)1463
{1464
if (sigil_is_undefined(shader_type_tag)) {1465
shader_type_tag = sigil_intern_symbol(vm, "sigil-graphics-shader", 21);1466
}1467
}1469
static GfxShader *get_shader(SigilVM *vm, Value v)1470
{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);1475
}1477
static void shader_free_resources(GfxShader *sh)1478
{1479
if (!sh || sh->freed) return;1480
if (active_shader == sh) {1481
active_shader = NULL;1482
}1483
if (sh->pipeline.id != SG_INVALID_ID) {1484
sg_destroy_pipeline(sh->pipeline);1485
}1486
if (sh->shader.id != SG_INVALID_ID) {1487
sg_destroy_shader(sh->shader);1488
}1489
sh->freed = true;1490
}1492
static void shader_destructor(void *data)1493
{1494
GfxShader *sh = (GfxShader *)data;1495
if (sh) {1496
shader_free_resources(sh);1497
free(sh);1498
}1499
}1501
/* Map a GLSL type token to sokol_gfx uniform type + size in bytes1502
* (NATIVE layout — same as STD140 except for vec3, which we don't use).1503
* Returns 0 on unrecognized type. */1504
static int map_glsl_type(const char *tok, sg_uniform_type *out_type, uint32_t *out_size)1505
{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;1512
}1514
/* Parse the user's fragment GLSL for `uniform <type> <name>;` declarations.1515
*1516
* Builds the GfxShader's uniform layout. Skips sampler2D — those are1517
* texture bindings, not uniform-block entries (the channel-0 binding is1518
* managed by the draw primitive). Recognised types: float, vec2, vec3,1519
* vec4, mat4. Unknown types are silently skipped (the shader-create call1520
* will fail later at link time if names mismatch — fine).1521
*1522
* NATIVE layout: tightly packed, no padding. */1523
static void parse_fragment_uniforms(GfxShader *sh, const char *frag_src)1524
{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 at1533
* 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 whitespace1537
* 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;1543
}1544
}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++;1552
}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;1559
}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;1565
}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++;1573
}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;1588
}1589
cursor += usize;1590
sh->num_uniforms++;1591
p = q;1592
}1593
sh->buffer_size = cursor;1594
memset(sh->buffer, 0, sizeof(sh->buffer));1595
}1597
/* (load-shader vertex-source fragment-source) -> <shader> or #f */1598
static Value native_load_shader(SigilVM *vm, int argc, Value *args)1599
{1600
if (argc < 2) {1601
sigil__vm_error(vm, SIGIL_ERR_ARITY,1602
"load-shader: requires vertex-source, fragment-source");1603
return SIGIL_UNDEFINED;1604
}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;1612
}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;1619
}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 writes1629
* must declare these inputs at the matching locations; for GL the1630
* 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 — matches1634
* sgp's default convention. The user fragment shader names this1635
* 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's1647
* SGP_UNIFORM_SLOT_FRAGMENT — sgp_flush emits fragment uniforms1648
* to slot 1 when sgp_set_uniform's fs_size > 0). NATIVE layout —1649
* tightly packed, alignment=1, matches my parser's offset1650
* computation. array_count must be >= 1 (sokol asserts > 0 in1651
* _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;1660
}1661
}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;1674
}1676
/* Build a custom sgp pipeline for this shader. Default to alpha blend1677
* since post-processing typically overlays a textured rect on the1678
* 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;1690
}1692
ensure_shader_type(vm);1693
return sigil_make_foreign(vm, shader_type_tag, sh, shader_destructor,1694
sizeof(GfxShader));1695
}1697
/* (shader? obj) -> boolean */1698
static Value native_shader_p(SigilVM *vm, int argc, Value *args)1699
{1700
(void)argc;1701
return get_shader(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;1702
}1704
/* (shader-free! shader) — explicit cleanup. Idempotent. */1705
static Value native_shader_free(SigilVM *vm, int argc, Value *args)1706
{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;1713
}1714
shader_free_resources(sh);1715
return SIGIL_NIL;1716
}1718
/* Find a uniform by name in the shader's layout. Returns -1 if absent. */1719
static int shader_uniform_index(const GfxShader *sh, const char *name)1720
{1721
for (int i = 0; i < sh->num_uniforms; i++) {1722
if (strcmp(sh->uniforms[i].name, name) == 0) return i;1723
}1724
return -1;1725
}1727
/* Refresh u_time + u_resolution into the active shader's buffer and1728
* push the buffer to the fragment stage uniform block. Called from1729
* draw primitives that use the active shader. */1730
static void apply_active_shader_uniforms(void)1731
{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));1738
}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;1744
}1745
memcpy(sh->buffer + sh->uniforms[sh->u_resolution_index].offset,1746
res, sizeof(res));1747
}1748
if (sh->buffer_size > 0) {1749
sgp_set_uniform(NULL, 0, sh->buffer, sh->buffer_size);1750
}1751
}1753
/* (set-shader-uniform shader name value) — write a value into the1754
* shader's uniform buffer at the offset matching `name`.1755
*1756
* Accepts:1757
* number → float1758
* list of 2 → vec21759
* list of 3 → vec31760
* list of 4 → vec41761
* 16 floats → mat4 (rare; pass as a list)1762
*1763
* sampler2D uniforms are NOT set through this path; channel 0 is bound1764
* by the draw primitive (e.g., draw-render-target) and additional1765
* samplers are out of scope for Phase 2. */1766
static Value native_set_shader_uniform(SigilVM *vm, int argc, Value *args)1767
{1768
if (argc < 3) {1769
sigil__vm_error(vm, SIGIL_ERR_ARITY,1770
"set-shader-uniform: requires shader, name, value");1771
return SIGIL_UNDEFINED;1772
}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;1778
}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]);1784
}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;1789
}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;1795
}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;1804
}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);1815
}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;1821
}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;1827
}1828
/* If this shader is currently active, push the updated buffer right1829
* away so the new value lands in the next draw without waiting for1830
* the next auto-refresh. */1831
if (active_shader == sh && sh->buffer_size > 0) {1832
sgp_set_uniform(NULL, 0, sh->buffer, sh->buffer_size);1833
}1834
return SIGIL_NIL;1835
}1837
/* (%bind-shader shader) — set as active sgp pipeline + uniform source. */1838
static Value native_bind_shader(SigilVM *vm, int argc, Value *args)1839
{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;1846
}1847
if (sh->freed) {1848
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,1849
"%bind-shader: shader has been freed");1850
return SIGIL_UNDEFINED;1851
}1852
sgp_set_pipeline(sh->pipeline);1853
active_shader = sh;1854
apply_active_shader_uniforms();1855
return SIGIL_NIL;1856
}1858
/* (%unbind-shader) — restore default sgp pipeline + clear active shader.1859
* sgp_reset_pipeline internally calls sgp_set_pipeline(INVALID), which1860
* memsets the uniform buffer; calling sgp_reset_uniform afterwards1861
* triggers an "invalid pipeline" assertion, so don't. */1862
static Value native_unbind_shader(SigilVM *vm, int argc, Value *args)1863
{1864
(void)vm; (void)argc; (void)args;1865
sgp_reset_pipeline();1866
active_shader = NULL;1867
return SIGIL_NIL;1868
}1870
/* ============================================================1871
* MODULE INITIALIZATION1872
* ============================================================ */1874
/* Forward declarations for related modules */1875
extern void sigil__init_sigil_graphics_image_module(SigilVM *vm);1876
extern void sigil__init_sigil_graphics_font_module(SigilVM *vm);1878
void sigil__init_sigil_graphics_module(SigilVM *vm)1879
{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 for1924
* 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 primitives1963
* 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 the1985
* 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);2053
}