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