Commitb10dbe99Recorded26 Apr 2026Repositorysigil-graphics

feat(graphics): add offscreen render targets (Phase 1)

Message

Adds make-render-target, with-render-target macro, render-target->texture, and draw-render-target — a minimal offscreen render-to-texture surface that lets downstream apps build post-processing pipelines (cinder-cantata v0.3 bloom/chromatic aberration is the immediate consumer).

Implementation: - GfxRenderTarget wraps an sgimage color attachment, an attachment view (for rendering INTO), a texture view + sampler (for sampling AS texture), and a paired depth-stencil image + view. The depth attachment is required because sgp's default pipelines bake in the swap-chain depth-stencil format; offscreen passes must match. - with-render-target uses sokolgp's nested sgpbegin/sgpend via the internal state stack, so the offscreen pass composes cleanly with begin-frame/end-frame: each render target owns its own sgpbegin/sgbeginpass/sgpflush/sgendpass/sgpend envelope inside the outer frame. - GfxTexture grows an ownsresources flag so render-target->texture can return a borrowed wrapper without double-freeing the rt's resources on GC. - with-render-target uses dynamic-wind so the offscreen pass closes cleanly on non-local exit, keeping the sgp/sg pass stacks balanced.

Build / runtime: - Install slogfunc as sgsetup's logger so validation failures print a diagnostic before sokol's abort. The implementation already lives in sigil-app's archive (sigil-app's sokol-app.c picks up SOKOLLOGIMPL via SOKOL_IMPL); graphics.c just declares it. - Add manifest.scm so guix shell -m manifest.scm can build + launch the test apps (mesa, libglvnd, libx11/xi/xcursor, alsa-lib, libogg/ libvorbis — mirrors cinder-cantata's runtime surface). - Bump sigil: ^0.13 → ^0.14 since the only sigil binary on PATH is 0.14.3; drop the explicit sigil-stdlib pin (auto-derived in 0.14).

Smoke test: test/test-render-target/ builds a 256×256 offscreen scene and composites it onto an 800×600 swap chain at four positions/sizes, including a tinted copy via set-color modulation. Clean exit; no sokol validation panics.

The cinder-side wiring at the phase boundary is deliberately not in this commit — it's throwaway test wiring that lives on a temp branch in the cinder repo per the task brief's "cinder integration" section.

Changed
 manifest.scm                                            |  28 +++++++++
 package.sgl                                             |   2 +-
 sigil.lock                                              |  15 +++--
 src/c/graphics.c                                        | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 src/c/sokol-graphics.c                                  |   3 +
 src/sigil/graphics.sgl                                  |  29 ++++++++-
 test/test-render-target/dev-redirects.sgl               |   8 +++
 test/test-render-target/package.sgl                     |  28 +++++++++
 test/test-render-target/src/test-render-target/main.sgl |  90 ++++++++++++++++++++++++++
 9 files changed, 657 insertions(+), 16 deletions(-)
Diff
manifest.scmadded
@@ -0,0 +1,28 @@
+1
;; sigil-graphics - Development Environment
+2
;; Use with: guix shell -m manifest.scm
+3
;;
+4
;; Mirrors cinder-cantata's runtime surface (mesa, X11 stack, alsa,
+5
;; ogg/vorbis) so the smoke tests under test/ launch cleanly under
+6
;; sokol_app on Guix. libglvnd is what supplies <GL/gl.h> on modern
+7
;; Guix; mesa alone is not enough for headers, only for the runtime
+8
;; libGL.
+9
+10
(specifications->manifest
+11
'("gcc-toolchain"
+12
"pkg-config"
+13
"binutils"
+14
+15
;; Graphics (sokol_gfx + sokol_app on Linux)
+16
"mesa" ; OpenGL implementation (libGL)
+17
"libglvnd" ; OpenGL dispatcher; provides GL/gl.h on modern Guix
+18
"libx11"
+19
"libxi"
+20
"libxcursor"
+21
+22
;; Audio (sokol_audio runtime — sigil-app pulls it in transitively)
+23
"alsa-lib"
+24
+25
;; OGG Vorbis (carried for parity with cinder's manifest, since
+26
;; downstream test apps may pull sigil-audio)
+27
"libvorbis"
+28
"libogg"))
package.sglmodified
@@ -6,6 +6,7 @@
6
(package
7
name: "sigil-graphics"
8
version: "0.9.3"
+9
sigil: "^0.14"
10
description: "2D graphics, image loading, and font rendering for Sigil"
11
url: "https://codeberg.org/sigil/sigil-graphics"
12
license: "BSD-3-Clause"
@@ -37,7 +38,6 @@
38
features: '(release cross windows)))
39
40
dependencies: (list
40
(from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.13.0")
41
(from-git url: "codeberg:sigil/sigil-app" version: "^0.8.2"))
42
43
libraries: (list
sigil.lockmodified
@@ -1,10 +1,17 @@
1
;; Auto-generated by sigil deps install. Do not edit.
2
(lock
+3
(package name: "sigil-lib"
+4
url: "codeberg:sigil/sigil-lang"
+5
ref: "^0.14"
+6
sha: "ff67dfe2033e03cb3d71ba3d801418ba0729094f"
+7
package-selector: "sigil-lib"
+8
version: "0.14.4")
9
(package name: "sigil-stdlib"
4
url: "codeberg:sigil/sigil"
5
ref: "^0.13.0"
6
sha: "2e95b0343b976057a7655b41f08f0930d6abb628"
7
package-selector: "sigil-stdlib")
+10
url: "codeberg:sigil/sigil-lang"
+11
ref: "^0.14"
+12
sha: "ff67dfe2033e03cb3d71ba3d801418ba0729094f"
+13
package-selector: "sigil-stdlib"
+14
version: "0.14.4")
15
(package name: "sigil-app"
16
url: "codeberg:sigil/sigil-app"
17
ref: "^0.8.2"
src/c/graphics.cmodified
@@ -11,6 +11,7 @@
11
#include "sokol_gfx.h"
12
#include "sokol_glue.h"
13
#include "sokol_gp.h"
+14
#include "sokol_log.h"
15
16
#include <stdio.h>
17
#include <stdlib.h>
@@ -36,15 +37,45 @@ static bool gfx_initialized = false;
37
/* Texture type tag (initialized at module init) */
38
static Value texture_type_tag = SIGIL_UNDEFINED;
39
39
/* Texture structure */
+40
/* Render target type tag (initialized at module init) */
+41
static Value rt_type_tag = SIGIL_UNDEFINED;
+42
+43
/* Texture structure.
+44
*
+45
* `owns_resources` is false for textures that borrow their handles from
+46
* another owner (e.g., from a render target via render-target->texture).
+47
* Borrowed wrappers must not destroy the underlying sokol resources;
+48
* the original owner does that. */
49
typedef struct {
50
sg_image handle;
51
sg_sampler sampler;
52
sg_view view;
53
int width;
54
int height;
+55
bool owns_resources;
56
} GfxTexture;
57
+58
/* Render target structure.
+59
*
+60
* Holds a color image, a color-attachment view (for rendering INTO),
+61
* a texture view (for sampling AS texture), a sampler, and a
+62
* depth-stencil image + view. The depth-stencil attachment is only
+63
* needed because sgp's default pipelines bake in the swap chain's
+64
* depth-stencil format; offscreen passes must provide a matching
+65
* attachment so pipeline validation passes. We don't actually use the
+66
* depth buffer for 2D rendering. */
+67
typedef struct {
+68
sg_image color_img;
+69
sg_view color_att_view;
+70
sg_view tex_view;
+71
sg_sampler sampler;
+72
sg_image depth_img;
+73
sg_view depth_att_view;
+74
int width;
+75
int height;
+76
bool freed;
+77
} GfxRenderTarget;
+78
79
/* Current draw color */
80
static float draw_color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
81
@@ -89,9 +120,11 @@ static Value native_gfx_setup(SigilVM *vm, int argc, Value *args)
120
return SIGIL_NIL;
121
}
122
92
/* Initialize sokol_gfx */
+123
/* Initialize sokol_gfx. Install slog_func so validation failures
+124
* print a useful diagnostic before sokol aborts. */
125
sg_desc desc = {
126
.environment = sglue_environment(),
+127
.logger.func = slog_func,
128
};
129
sg_setup(&desc);
130
@@ -700,14 +733,16 @@ static void texture_destructor(void *data)
733
{
734
GfxTexture *tex = (GfxTexture *)data;
735
if (tex) {
703
if (tex->view.id != SG_INVALID_ID) {
704
sg_destroy_view(tex->view);
705
}
706
if (tex->handle.id != SG_INVALID_ID) {
707
sg_destroy_image(tex->handle);
708
}
709
if (tex->sampler.id != SG_INVALID_ID) {
710
sg_destroy_sampler(tex->sampler);
+736
if (tex->owns_resources) {
+737
if (tex->view.id != SG_INVALID_ID) {
+738
sg_destroy_view(tex->view);
+739
}
+740
if (tex->handle.id != SG_INVALID_ID) {
+741
sg_destroy_image(tex->handle);
+742
}
+743
if (tex->sampler.id != SG_INVALID_ID) {
+744
sg_destroy_sampler(tex->sampler);
+745
}
746
}
747
free(tex);
748
}
@@ -783,6 +818,7 @@ static Value native_load_texture(SigilVM *vm, int argc, Value *args)
818
tex->view = view;
819
tex->width = width;
820
tex->height = height;
+821
tex->owns_resources = true;
822
823
ensure_texture_type(vm);
824
return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
@@ -909,6 +945,390 @@ static Value native_draw_texture_region(SigilVM *vm, int argc, Value *args)
945
return SIGIL_NIL;
946
}
947
+948
/* ============================================================
+949
* RENDER TARGETS
+950
* ============================================================ */
+951
+952
static void ensure_rt_type(SigilVM *vm)
+953
{
+954
if (sigil_is_undefined(rt_type_tag)) {
+955
rt_type_tag = sigil_intern_symbol(vm, "sigil-graphics-rt", 17);
+956
}
+957
}
+958
+959
static GfxRenderTarget *get_rt(SigilVM *vm, Value v)
+960
{
+961
if (!sigil_is_foreign(v)) return NULL;
+962
ensure_rt_type(vm);
+963
if (sigil_foreign_type(v) != rt_type_tag) return NULL;
+964
return (GfxRenderTarget *)sigil_foreign_data(v);
+965
}
+966
+967
/* Free a render target's GPU resources. Idempotent. */
+968
static void rt_free_resources(GfxRenderTarget *rt)
+969
{
+970
if (!rt || rt->freed) return;
+971
if (rt->color_att_view.id != SG_INVALID_ID) {
+972
sg_destroy_view(rt->color_att_view);
+973
}
+974
if (rt->tex_view.id != SG_INVALID_ID) {
+975
sg_destroy_view(rt->tex_view);
+976
}
+977
if (rt->depth_att_view.id != SG_INVALID_ID) {
+978
sg_destroy_view(rt->depth_att_view);
+979
}
+980
if (rt->color_img.id != SG_INVALID_ID) {
+981
sg_destroy_image(rt->color_img);
+982
}
+983
if (rt->depth_img.id != SG_INVALID_ID) {
+984
sg_destroy_image(rt->depth_img);
+985
}
+986
if (rt->sampler.id != SG_INVALID_ID) {
+987
sg_destroy_sampler(rt->sampler);
+988
}
+989
rt->freed = true;
+990
}
+991
+992
static void rt_destructor(void *data)
+993
{
+994
GfxRenderTarget *rt = (GfxRenderTarget *)data;
+995
if (rt) {
+996
rt_free_resources(rt);
+997
free(rt);
+998
}
+999
}
+1000
+1001
/*
+1002
* (%make-render-target width height) -> <render-target> or #f
+1003
*
+1004
* Creates an offscreen color render target backed by an sg_image with
+1005
* color_attachment usage, plus the views and sampler needed to render
+1006
* into it and sample it as a texture.
+1007
*/
+1008
static Value native_make_render_target(SigilVM *vm, int argc, Value *args)
+1009
{
+1010
if (argc < 2) {
+1011
sigil__vm_error(vm, SIGIL_ERR_ARITY,
+1012
"make-render-target: requires width, height arguments");
+1013
return SIGIL_UNDEFINED;
+1014
}
+1015
+1016
int w = (int)value_to_float(args[0]);
+1017
int h = (int)value_to_float(args[1]);
+1018
if (w <= 0 || h <= 0) {
+1019
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1020
"make-render-target: width and height must be positive");
+1021
return SIGIL_FALSE;
+1022
}
+1023
+1024
/* Pixel format and sample count default to sg_environment.defaults
+1025
* (i.e., the swap chain's color format / sample count). Matching them
+1026
* is required so sgp's default pipelines validate against this pass —
+1027
* sgp pipelines bake in the format/sample count from sgp_setup. */
+1028
sg_image_desc img_desc = {
+1029
.usage = { .color_attachment = true },
+1030
.width = w,
+1031
.height = h,
+1032
};
+1033
sg_image img = sg_make_image(&img_desc);
+1034
if (img.id == SG_INVALID_ID) {
+1035
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
+1036
"make-render-target: failed to create color image");
+1037
return SIGIL_FALSE;
+1038
}
+1039
+1040
sg_view_desc att_desc = {
+1041
.color_attachment = { .image = img },
+1042
.label = "sigil-rt-color-attachment",
+1043
};
+1044
sg_view att_view = sg_make_view(&att_desc);
+1045
if (att_view.id == SG_INVALID_ID) {
+1046
sg_destroy_image(img);
+1047
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
+1048
"make-render-target: failed to create color attachment view");
+1049
return SIGIL_FALSE;
+1050
}
+1051
+1052
sg_view tex_view = sgp_make_texture_view_from_image(img, "sigil-rt-texture");
+1053
if (tex_view.id == SG_INVALID_ID) {
+1054
sg_destroy_view(att_view);
+1055
sg_destroy_image(img);
+1056
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
+1057
"make-render-target: failed to create texture view");
+1058
return SIGIL_FALSE;
+1059
}
+1060
+1061
/* Depth-stencil attachment — needed only for pipeline-validation
+1062
* compatibility with sgp's default pipelines, which bake in the
+1063
* swap chain's depth-stencil format. */
+1064
sg_image_desc depth_desc = {
+1065
.usage = { .depth_stencil_attachment = true },
+1066
.width = w,
+1067
.height = h,
+1068
};
+1069
sg_image depth_img = sg_make_image(&depth_desc);
+1070
if (depth_img.id == SG_INVALID_ID) {
+1071
sg_destroy_view(tex_view);
+1072
sg_destroy_view(att_view);
+1073
sg_destroy_image(img);
+1074
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
+1075
"make-render-target: failed to create depth image");
+1076
return SIGIL_FALSE;
+1077
}
+1078
+1079
sg_view_desc depth_att_desc = {
+1080
.depth_stencil_attachment = { .image = depth_img },
+1081
.label = "sigil-rt-depth-attachment",
+1082
};
+1083
sg_view depth_att_view = sg_make_view(&depth_att_desc);
+1084
if (depth_att_view.id == SG_INVALID_ID) {
+1085
sg_destroy_image(depth_img);
+1086
sg_destroy_view(tex_view);
+1087
sg_destroy_view(att_view);
+1088
sg_destroy_image(img);
+1089
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
+1090
"make-render-target: failed to create depth attachment view");
+1091
return SIGIL_FALSE;
+1092
}
+1093
+1094
sg_sampler_desc smp_desc = {
+1095
.min_filter = SG_FILTER_LINEAR,
+1096
.mag_filter = SG_FILTER_LINEAR,
+1097
.wrap_u = SG_WRAP_CLAMP_TO_EDGE,
+1098
.wrap_v = SG_WRAP_CLAMP_TO_EDGE,
+1099
};
+1100
sg_sampler smp = sg_make_sampler(&smp_desc);
+1101
+1102
GfxRenderTarget *rt = malloc(sizeof(GfxRenderTarget));
+1103
if (!rt) {
+1104
sg_destroy_sampler(smp);
+1105
sg_destroy_view(depth_att_view);
+1106
sg_destroy_image(depth_img);
+1107
sg_destroy_view(tex_view);
+1108
sg_destroy_view(att_view);
+1109
sg_destroy_image(img);
+1110
sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
+1111
"make-render-target: out of memory");
+1112
return SIGIL_FALSE;
+1113
}
+1114
+1115
rt->color_img = img;
+1116
rt->color_att_view = att_view;
+1117
rt->tex_view = tex_view;
+1118
rt->sampler = smp;
+1119
rt->depth_img = depth_img;
+1120
rt->depth_att_view = depth_att_view;
+1121
rt->width = w;
+1122
rt->height = h;
+1123
rt->freed = false;
+1124
+1125
ensure_rt_type(vm);
+1126
return sigil_make_foreign(vm, rt_type_tag, rt, rt_destructor,
+1127
sizeof(GfxRenderTarget));
+1128
}
+1129
+1130
/*
+1131
* (render-target? obj) -> boolean
+1132
*/
+1133
static Value native_render_target_p(SigilVM *vm, int argc, Value *args)
+1134
{
+1135
(void)argc;
+1136
return get_rt(vm, args[0]) ? SIGIL_TRUE : SIGIL_FALSE;
+1137
}
+1138
+1139
/*
+1140
* (render-target-width rt) -> integer
+1141
*/
+1142
static Value native_render_target_width(SigilVM *vm, int argc, Value *args)
+1143
{
+1144
(void)argc;
+1145
GfxRenderTarget *rt = get_rt(vm, args[0]);
+1146
if (!rt) {
+1147
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1148
"render-target-width: expected render-target");
+1149
return SIGIL_UNDEFINED;
+1150
}
+1151
return sigil_fixnum(rt->width);
+1152
}
+1153
+1154
/*
+1155
* (render-target-height rt) -> integer
+1156
*/
+1157
static Value native_render_target_height(SigilVM *vm, int argc, Value *args)
+1158
{
+1159
(void)argc;
+1160
GfxRenderTarget *rt = get_rt(vm, args[0]);
+1161
if (!rt) {
+1162
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1163
"render-target-height: expected render-target");
+1164
return SIGIL_UNDEFINED;
+1165
}
+1166
return sigil_fixnum(rt->height);
+1167
}
+1168
+1169
/*
+1170
* (render-target-free! rt) - Explicit cleanup of GPU resources
+1171
*
+1172
* Idempotent. After this call the render target is unusable; the
+1173
* destructor on GC will be a no-op.
+1174
*/
+1175
static Value native_render_target_free(SigilVM *vm, int argc, Value *args)
+1176
{
+1177
(void)argc;
+1178
GfxRenderTarget *rt = get_rt(vm, args[0]);
+1179
if (!rt) {
+1180
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1181
"render-target-free!: expected render-target");
+1182
return SIGIL_UNDEFINED;
+1183
}
+1184
rt_free_resources(rt);
+1185
return SIGIL_NIL;
+1186
}
+1187
+1188
/*
+1189
* (%begin-rt-pass rt) - Push a new sokol_gp queue + sokol_gfx pass that
+1190
* targets the render target. All subsequent draws land in rt's color
+1191
* image until %end-rt-pass is called.
+1192
*/
+1193
static Value native_begin_rt_pass(SigilVM *vm, int argc, Value *args)
+1194
{
+1195
(void)argc;
+1196
GfxRenderTarget *rt = get_rt(vm, args[0]);
+1197
if (!rt) {
+1198
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1199
"%begin-rt-pass: expected render-target");
+1200
return SIGIL_UNDEFINED;
+1201
}
+1202
if (rt->freed) {
+1203
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
+1204
"%begin-rt-pass: render target has been freed");
+1205
return SIGIL_UNDEFINED;
+1206
}
+1207
+1208
/* Push an inner sgp state on the stack with the render target's size.
+1209
* sgp's state stack ensures inner sgp_flush only emits commands queued
+1210
* between this sgp_begin and its matching sgp_end. */
+1211
sgp_begin(rt->width, rt->height);
+1212
sgp_viewport(0, 0, rt->width, rt->height);
+1213
sgp_project(0, (float)rt->width, 0, (float)rt->height);
+1214
+1215
sg_pass_action act = {
+1216
.colors[0] = {
+1217
.load_action = SG_LOADACTION_CLEAR,
+1218
.store_action = SG_STOREACTION_STORE,
+1219
.clear_value = {0.0f, 0.0f, 0.0f, 0.0f},
+1220
},
+1221
.depth = {
+1222
.load_action = SG_LOADACTION_CLEAR,
+1223
.store_action = SG_STOREACTION_DONTCARE,
+1224
.clear_value = 1.0f,
+1225
},
+1226
};
+1227
sg_pass pass = {
+1228
.action = act,
+1229
.attachments = {
+1230
.colors[0] = rt->color_att_view,
+1231
.depth_stencil = rt->depth_att_view,
+1232
},
+1233
};
+1234
sg_begin_pass(&pass);
+1235
+1236
return SIGIL_NIL;
+1237
}
+1238
+1239
/*
+1240
* (%end-rt-pass) - Flush queued commands to the current render target,
+1241
* end the sokol_gfx pass, and pop the inner sgp state.
+1242
*
+1243
* Must balance a prior %begin-rt-pass call.
+1244
*/
+1245
static Value native_end_rt_pass(SigilVM *vm, int argc, Value *args)
+1246
{
+1247
(void)vm; (void)argc; (void)args;
+1248
+1249
sgp_flush();
+1250
sg_end_pass();
+1251
sgp_end();
+1252
+1253
return SIGIL_NIL;
+1254
}
+1255
+1256
/*
+1257
* (render-target->texture rt) -> <texture>
+1258
*
+1259
* Returns a texture wrapper that borrows the render target's image,
+1260
* texture view, and sampler. The returned texture is invalid after the
+1261
* render target is freed; callers must keep the render target alive for
+1262
* the lifetime of the wrapper.
+1263
*/
+1264
static Value native_render_target_to_texture(SigilVM *vm, int argc, Value *args)
+1265
{
+1266
(void)argc;
+1267
GfxRenderTarget *rt = get_rt(vm, args[0]);
+1268
if (!rt) {
+1269
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1270
"render-target->texture: expected render-target");
+1271
return SIGIL_UNDEFINED;
+1272
}
+1273
+1274
GfxTexture *tex = malloc(sizeof(GfxTexture));
+1275
if (!tex) {
+1276
sigil__vm_set_error(vm, SIGIL_ERR_MEMORY,
+1277
"render-target->texture: out of memory");
+1278
return SIGIL_FALSE;
+1279
}
+1280
+1281
tex->handle = rt->color_img;
+1282
tex->sampler = rt->sampler;
+1283
tex->view = rt->tex_view;
+1284
tex->width = rt->width;
+1285
tex->height = rt->height;
+1286
tex->owns_resources = false;
+1287
+1288
ensure_texture_type(vm);
+1289
return sigil_make_foreign(vm, texture_type_tag, tex, texture_destructor,
+1290
sizeof(GfxTexture));
+1291
}
+1292
+1293
/*
+1294
* (draw-render-target rt x y w h) - Draw the render target's color image
+1295
* as a textured rect on the current pass.
+1296
*
+1297
* Convenience over (draw-texture (render-target->texture rt) x y w h):
+1298
* skips the texture wrapper allocation.
+1299
*/
+1300
static Value native_draw_render_target(SigilVM *vm, int argc, Value *args)
+1301
{
+1302
if (argc < 5) {
+1303
sigil__vm_error(vm, SIGIL_ERR_ARITY,
+1304
"draw-render-target: requires rt, x, y, w, h");
+1305
return SIGIL_UNDEFINED;
+1306
}
+1307
GfxRenderTarget *rt = get_rt(vm, args[0]);
+1308
if (!rt) {
+1309
sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
+1310
"draw-render-target: expected render-target");
+1311
return SIGIL_UNDEFINED;
+1312
}
+1313
+1314
float x = value_to_float(args[1]);
+1315
float y = value_to_float(args[2]);
+1316
float w = value_to_float(args[3]);
+1317
float h = value_to_float(args[4]);
+1318
+1319
sgp_set_view(0, rt->tex_view);
+1320
sgp_set_sampler(0, rt->sampler);
+1321
+1322
sgp_rect dest = {x, y, w, h};
+1323
sgp_rect src = {0, 0, (float)rt->width, (float)rt->height};
+1324
sgp_draw_textured_rect(0, dest, src);
+1325
+1326
sgp_reset_view(0);
+1327
sgp_reset_sampler(0);
+1328
+1329
return SIGIL_NIL;
+1330
}
+1331
1332
/* ============================================================
1333
* MODULE INITIALIZATION
1334
* ============================================================ */
@@ -1001,6 +1421,27 @@ void sigil__init_sigil_graphics_module(SigilVM *vm)
1421
sigil_module_register_native(vm, "draw-texture-region", native_draw_texture_region,
1422
SIGIL_ARITY_EXACT(9), "Draw texture region");
1423
+1424
/* Render targets — %begin-rt-pass / %end-rt-pass are raw primitives
+1425
* wrapped by the with-render-target macro for stack discipline. */
+1426
sigil_module_register_native(vm, "%make-render-target", native_make_render_target,
+1427
SIGIL_ARITY_EXACT(2), "Create offscreen render target");

Showing the first 500 of 536 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.

src/c/sokol-graphics.cmodified
@@ -20,3 +20,6 @@
20
#include "sokol_gfx.h"
21
#include "sokol_glue.h"
22
#include "sokol_gp.h"
+23
/* slog_func is implemented in sigil-app's archive (sigil-app's
+24
* sokol-app.c picks up SOKOL_LOG_IMPL via SOKOL_IMPL). graphics.c
+25
* just declares it via sokol_log.h. */
src/sigil/graphics.sglmodified
@@ -43,6 +43,14 @@
43
texture? texture-width texture-height
44
draw-texture draw-texture-region
45
+46
;; Render targets (offscreen render-to-texture)
+47
make-render-target
+48
render-target? render-target-width render-target-height
+49
render-target-free!
+50
render-target->texture
+51
draw-render-target
+52
with-render-target
+53
54
;; Re-export image functions for convenience
55
load-image
56
image? image-width image-height image-channels
@@ -96,4 +104,23 @@
104
(define-syntax with-additive-blend
105
(syntax-rules ()
106
((_ body ...)
99
(with-blend-mode 'additive body ...))))))
+107
(with-blend-mode 'additive body ...))))
+108
+109
;; Public render-target constructor — wraps the raw native to keep
+110
;; the surface name additive (the existing C native is %make-render-target).
+111
(define (make-render-target width height)
+112
(%make-render-target width height))
+113
+114
;; Run body with the render target bound as the active draw surface.
+115
;; All draws inside body land in rt's color image. Uses dynamic-wind
+116
;; so the offscreen pass is properly closed even on non-local exit
+117
;; (exception, continuation invocation), keeping the sgp/sg pass
+118
;; stacks balanced for the next frame.
+119
(define-syntax with-render-target
+120
(syntax-rules ()
+121
((_ rt body ...)
+122
(let ((target rt))
+123
(dynamic-wind
+124
(lambda () (%begin-rt-pass target))
+125
(lambda () body ...)
+126
(lambda () (%end-rt-pass)))))))))
test/test-render-target/dev-redirects.sgladded
@@ -0,0 +1,8 @@
+1
;; Local sigil-graphics is the in-progress version under development —
+2
;; the offscreen render target work this test exercises lives two
+3
;; directories up. sigil-app uses the resolved registry version.
+4
(redirects
+5
repos: (list
+6
(for-repo
+7
url: "codeberg:sigil/sigil-graphics"
+8
use: (from-path dir: "../.."))))
test/test-render-target/package.sgladded
@@ -0,0 +1,28 @@
+1
;;; test-render-target — smoke test for sigil-graphics render targets
+2
;;;
+3
;;; Renders a coloured quad into a 256×256 offscreen render target, then
+4
;;; composites the target back onto the swap chain at four different
+5
;;; positions and sizes. Quits after ~4 seconds or when escape is pressed.
+6
+7
(package
+8
name: "test-render-target"
+9
version: "0.0.1"
+10
sigil: "^0.14"
+11
description: "Smoke test: offscreen render target end-to-end"
+12
license: "BSD-3-Clause"
+13
authors: (list "David Wilson <[email protected]>")
+14
+15
entry: '(test-render-target main)
+16
bundle-name: "test-render-target"
+17
+18
configs: (list
+19
(config
+20
name: 'dev
+21
output-dir: "build/dev"
+22
debug?: #t
+23
optimize: 0
+24
bundle?: #t))
+25
+26
dependencies: (list
+27
(from-git url: "codeberg:sigil/sigil-app" version: "^0.8.2")
+28
(from-git url: "codeberg:sigil/sigil-graphics" version: "^0.9.3")))
test/test-render-target/src/test-render-target/main.sgladded
@@ -0,0 +1,90 @@
+1
;;; test-render-target / main — render target end-to-end smoke test
+2
;;;
+3
;;; Validates the offscreen render target API by building a small scene
+4
;;; (a magenta backdrop with a green and a red filled rect plus a yellow
+5
;;; circle) into a 256×256 render target, then drawing that render target
+6
;;; back onto the swap chain at four positions and sizes. If render
+7
;;; targets are wired correctly the four composited copies are visually
+8
;;; identical and contain the scene drawn at offscreen coordinates.
+9
+10
(define-library (test-render-target main)
+11
(import (sigil core)
+12
(sigil math)
+13
(sigil app)
+14
(sigil graphics))
+15
(export main)
+16
+17
(begin
+18
+19
(define WINDOW-W 800)
+20
(define WINDOW-H 600)
+21
(define RT-SIZE 256)
+22
+23
;; Renders the offscreen scene into the active draw target.
+24
;; Coordinates are in render-target space (0,0)..(RT-SIZE,RT-SIZE).
+25
(define (draw-rt-scene)
+26
;; Magenta backdrop so the offscreen surface is obvious if it's
+27
;; ever sampled un-filled.
+28
(set-color 0.6 0.1 0.6 1.0)
+29
(draw-filled-rect 0.0 0.0 (inexact RT-SIZE) (inexact RT-SIZE))
+30
;; Green and red rectangles to give the composited image an
+31
;; identifiable orientation.
+32
(set-color 0.1 0.9 0.2 1.0)
+33
(draw-filled-rect 32.0 32.0 96.0 64.0)
+34
(set-color 0.95 0.2 0.2 1.0)
+35
(draw-filled-rect 128.0 96.0 96.0 96.0)
+36
;; Yellow circle in the bottom-right so we can see that
+37
;; circle drawing also works inside an offscreen pass.
+38
(set-color 1.0 0.92 0.1 1.0)
+39
(draw-filled-circle 200.0 210.0 28.0 32))
+40
+41
(define (game-loop)
+42
(gfx-setup)
+43
(set-viewport WINDOW-W WINDOW-H)
+44
(let* ((rt (make-render-target RT-SIZE RT-SIZE)))
+45
;; Sanity: surface predicates and accessors agree with the
+46
;; constructor before we start rendering with the target.
+47
(when (not (render-target? rt))
+48
(display "FAIL: render-target? returned false\n"))
+49
(when (not (= (render-target-width rt) RT-SIZE))
+50
(display "FAIL: render-target-width mismatch\n"))
+51
(when (not (= (render-target-height rt) RT-SIZE))
+52
(display "FAIL: render-target-height mismatch\n"))
+53
+54
(let loop ((elapsed 0.0))
+55
(let ((dt (wait-frame)))
+56
;; Refresh the offscreen target every frame — proves the
+57
;; pass-restart path is stable, not just a one-shot.
+58
(with-render-target rt
+59
(draw-rt-scene))
+60
+61
(with-frame
+62
;; Dim swap-chain background so the four RT copies stand
+63
;; out against negative space.
+64
(clear-screen 0.05 0.05 0.08 1.0)
+65
+66
;; Native size at the top-left corner.
+67
(draw-render-target rt 16.0 16.0
+68
(inexact RT-SIZE) (inexact RT-SIZE))
+69
;; Half-size top-right.
+70
(draw-render-target rt 528.0 16.0 128.0 128.0)
+71
;; Stretched wide on the bottom row.
+72
(draw-render-target rt 16.0 320.0 384.0 256.0)
+73
;; Tinted by the current draw color (set-color is a
+74
;; modulator on textured draws); pale blue tint.
+75
(set-color 0.6 0.85 1.0 1.0)
+76
(draw-render-target rt 432.0 320.0 256.0 256.0)
+77
(set-color 1.0 1.0 1.0 1.0))
+78
+79
(cond
+80
((key-pressed? 'escape) (request-quit))
+81
((quit-requested?) #t)
+82
((> elapsed 4.0) (request-quit))
+83
(else (loop (+ elapsed dt))))))
+84
(render-target-free! rt))
+85
(gfx-shutdown))
+86
+87
(define (main . _args)
+88
(run-game "sigil-graphics render-target smoke test"
+89
WINDOW-W WINDOW-H game-loop)
+90
0)))