Commit6d068a30Recorded19 Sep 2026Repositorysigil-graphics
Report a failed shader, view or pipeline and an oversized uniform block as errors
Message
From the adversarial review of the branch:
- sg_make_shader, sg_make_view and sgp_make_pipeline return a usable-looking id for a resource in FAILED state. load-shader checked only for the invalid id, so a GLSL compile error went on to sgp_make_pipeline and, in a dev build, sokol's validation panic aborted the process instead of load-shader returning an error. Every creation site now checks the resource state and releases the slot.
- The uniform parser silently dropped the current and all later declarations past 16 members or past the 1 KiB block, and sokol only warns about names the program lacks, never about program uniforms the desc omits. Both caps are now load-shader errors naming the limit.
- flatten_floats bounds its recursion (a cyclic vector or list is an error, not a stack overflow).
- test-shader-limits probes all three refusals; the gl-log sub-arm expects exactly one deliberate compile-failure line, which also proves the log is being captured.
Changed
src/c/graphics.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++--------------------
test/test-shader-limits/src/test-shader-limits/scene.sgl | 15 +++++++++++++++
test/test-shader-limits/verify.mjs | 16 +++++++++++++---
3 files changed, 74 insertions(+), 23 deletions(-)Diff
src/c/graphics.cmodified
@@ -946,7 +946,8 @@ static Value native_load_texture(SigilVM *vm, int argc, Value *args)
946
947
/* Create texture view from image (required by new sokol_gp API) */ 948
sg_view view = sgp_make_texture_view_from_image(img, "sigil-texture");−949
if (view.id == SG_INVALID_ID) {+949
if (view.id == SG_INVALID_ID || sg_query_view_state(view) != SG_RESOURCESTATE_VALID) {+950
if (view.id != SG_INVALID_ID) sg_destroy_view(view); 951
sg_destroy_image(img); 952
sg_destroy_sampler(smp); 953
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,@@ -1092,7 +1093,8 @@ static Value native_make_texture_from_pixels(SigilVM *vm, int argc, Value *args)
1093
sg_sampler smp = sg_make_sampler(&smp_desc); 1094
1095
sg_view view = sgp_make_texture_view_from_image(img, "sigil-pixel-texture");−1095
if (view.id == SG_INVALID_ID) {+1096
if (view.id == SG_INVALID_ID || sg_query_view_state(view) != SG_RESOURCESTATE_VALID) {+1097
if (view.id != SG_INVALID_ID) sg_destroy_view(view); 1098
sg_destroy_image(img); 1099
sg_destroy_sampler(smp); 1100
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,@@ -1401,7 +1403,8 @@ static Value native_make_render_target(SigilVM *vm, int argc, Value *args)
1403
.label = "sigil-rt-color-attachment", 1404
}; 1405
sg_view att_view = sg_make_view(&att_desc);−1404
if (att_view.id == SG_INVALID_ID) {+1406
if (att_view.id == SG_INVALID_ID || sg_query_view_state(att_view) != SG_RESOURCESTATE_VALID) {+1407
if (att_view.id != SG_INVALID_ID) sg_destroy_view(att_view); 1408
sg_destroy_image(img); 1409
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, 1410
"make-render-target: failed to create color attachment view");@@ -1409,7 +1412,8 @@ static Value native_make_render_target(SigilVM *vm, int argc, Value *args)
1412
} 1413
1414
sg_view tex_view = sgp_make_texture_view_from_image(img, "sigil-rt-texture");−1412
if (tex_view.id == SG_INVALID_ID) {+1415
if (tex_view.id == SG_INVALID_ID || sg_query_view_state(tex_view) != SG_RESOURCESTATE_VALID) {+1416
if (tex_view.id != SG_INVALID_ID) sg_destroy_view(tex_view); 1417
sg_destroy_view(att_view); 1418
sg_destroy_image(img); 1419
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,@@ -1452,7 +1456,8 @@ static Value native_make_render_target(SigilVM *vm, int argc, Value *args)
1456
.label = "sigil-rt-depth-attachment", 1457
}; 1458
depth_att_view = sg_make_view(&depth_att_desc);−1455
if (depth_att_view.id == SG_INVALID_ID) {+1459
if (depth_att_view.id == SG_INVALID_ID || sg_query_view_state(depth_att_view) != SG_RESOURCESTATE_VALID) {+1460
if (depth_att_view.id != SG_INVALID_ID) sg_destroy_view(depth_att_view); 1461
sg_destroy_image(depth_img); 1462
sg_destroy_view(tex_view); 1463
sg_destroy_view(att_view);@@ -1800,12 +1805,17 @@ static int is_glsl_space(char c)
1805
* 1806
* NATIVE layout: tightly packed, no padding — arrays included, which is 1807
* what sokol_gfx's _sg_uniform_size computes for SG_UNIFORMLAYOUT_NATIVE−1803
* and what glUniform*fv consumes. Stops at SIGIL_GFX_MAX_UNIFORMS members−1804
* (sokol's SG_MAX_UNIFORMBLOCK_MEMBERS) or when the block would exceed−1805
* SIGIL_GFX_UNIFORM_BUFFER_SIZE; a truncated layout links with a name−1806
* mismatch rather than silently dropping a member (sokol's GL backend−1807
* logs the missing uniform). */−1808
static void parse_fragment_uniforms(GfxShader *sh, const char *frag_src)+1808
* and what glUniform*fv consumes. A 17th member (sokol's+1809
* SG_MAX_UNIFORMBLOCK_MEMBERS) or a block past SIGIL_GFX_UNIFORM_BUFFER_SIZE+1810
* is reported to load-shader as an error rather than dropped: sokol only+1811
* warns about names in the desc that the program lacks, never about+1812
* program uniforms the desc omits, so a silently dropped member would+1813
* read as zero with nothing logged. */+1814
/* Returns 0, or SIGIL_GFX_PARSE_TOO_MANY / SIGIL_GFX_PARSE_TOO_LARGE when a+1815
* declaration does not fit; the caller (load-shader) reports it. */+1816
#define SIGIL_GFX_PARSE_TOO_MANY 1+1817
#define SIGIL_GFX_PARSE_TOO_LARGE 2+1818
static int parse_fragment_uniforms(GfxShader *sh, const char *frag_src) 1819
{ 1820
const char *p = frag_src; 1821
uint32_t cursor = 0;@@ -1887,8 +1897,8 @@ static void parse_fragment_uniforms(GfxShader *sh, const char *frag_src)
1897
array_count = (int)n; 1898
} 1899
uint32_t usize = elem_size * (uint32_t)array_count;−1890
if (sh->num_uniforms >= SIGIL_GFX_MAX_UNIFORMS) break;−1891
if (cursor + usize > sizeof(sh->buffer)) break;+1900
if (sh->num_uniforms >= SIGIL_GFX_MAX_UNIFORMS) return SIGIL_GFX_PARSE_TOO_MANY;+1901
if (cursor + usize > sizeof(sh->buffer)) return SIGIL_GFX_PARSE_TOO_LARGE; 1902
GfxUniformEntry *ent = &sh->uniforms[sh->num_uniforms]; 1903
strncpy(ent->name, name_buf, sizeof(ent->name)-1); 1904
ent->name[sizeof(ent->name)-1] = '\0';@@ -1908,6 +1918,7 @@ static void parse_fragment_uniforms(GfxShader *sh, const char *frag_src)
1918
} 1919
sh->buffer_size = cursor; 1920
memset(sh->buffer, 0, sizeof(sh->buffer));+1921
return 0; 1922
} 1923
1924
/* (load-shader vertex-source fragment-source [blend]) -> <shader> or #f@@ -1953,7 +1964,17 @@ static Value native_load_shader(SigilVM *vm, int argc, Value *args)
1964
sh->shader.id = SG_INVALID_ID; 1965
sh->pipeline.id = SG_INVALID_ID; 1966
−1956
parse_fragment_uniforms(sh, fs_src);+1967
int parse_rc = parse_fragment_uniforms(sh, fs_src);+1968
if (parse_rc != 0) {+1969
char msg[200];+1970
snprintf(msg, sizeof(msg), parse_rc == SIGIL_GFX_PARSE_TOO_MANY+1971
? "load-shader: more than %d uniform declarations (sokol_gfx's per-block cap; an array counts once)"+1972
: "load-shader: uniform declarations exceed the %d-byte block (SIGIL_GFX_UNIFORM_BUFFER_SIZE)",+1973
parse_rc == SIGIL_GFX_PARSE_TOO_MANY ? SIGIL_GFX_MAX_UNIFORMS : SIGIL_GFX_UNIFORM_BUFFER_SIZE);+1974
free(sh);+1975
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, msg);+1976
return SIGIL_UNDEFINED;+1977
} 1978
1979
sg_shader_desc desc = {0}; 1980
/* Vertex attributes — must match sgp's vertex layout (location 0 =@@ -1999,7 +2020,9 @@ static Value native_load_shader(SigilVM *vm, int argc, Value *args)
2020
desc.fragment_func.source = fs_src; 2021
2022
sh->shader = sg_make_shader(&desc);−2002
if (sh->shader.id == SG_INVALID_ID) {+2023
if (sh->shader.id == SG_INVALID_ID || sg_query_shader_state(sh->shader) != SG_RESOURCESTATE_VALID) {+2024
/* a compile/link failure leaves a FAILED slot with a real id */+2025
if (sh->shader.id != SG_INVALID_ID) sg_destroy_shader(sh->shader); 2026
free(sh); 2027
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME, 2028
"load-shader: shader compile/link failed (see sokol log above)");@@ -2014,7 +2037,8 @@ static Value native_load_shader(SigilVM *vm, int argc, Value *args)
2037
pip_desc.blend_mode = (sgp_blend_mode)blend; 2038
pip_desc.has_vs_color = true; /* sgp's vertex layout always has color */ 2039
sh->pipeline = sgp_make_pipeline(&pip_desc);−2017
if (sh->pipeline.id == SG_INVALID_ID) {+2040
if (sh->pipeline.id == SG_INVALID_ID || sg_query_pipeline_state(sh->pipeline) != SG_RESOURCESTATE_VALID) {+2041
if (sh->pipeline.id != SG_INVALID_ID) sg_destroy_pipeline(sh->pipeline); 2042
sg_destroy_shader(sh->shader); 2043
free(sh); 2044
sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,@@ -2093,8 +2117,10 @@ static void apply_active_shader_uniforms(void)
2117
/* Flatten a number / list / vector (nested one level or more) into 2118
* `out`, at most `cap` floats. Returns the count written, or -1 if a 2119
* non-number leaf was met or the count would exceed `cap`. */−2096
static int flatten_floats(Value v, float *out, int cap, int n)+2120
#define SIGIL_GFX_FLATTEN_MAX_DEPTH 8+2121
static int flatten_floats(Value v, float *out, int cap, int n, int depth) 2122
{+2123
if (depth > SIGIL_GFX_FLATTEN_MAX_DEPTH) return -1; /* a cyclic structure, or absurd nesting */ 2124
if (sigil_is_fixnum(v) || sigil_is_flonum(v)) { 2125
if (n >= cap) return -1; 2126
out[n] = value_to_float(v);@@ -2103,7 +2129,7 @@ static int flatten_floats(Value v, float *out, int cap, int n)
2129
if (sigil_is_pair(v) || sigil_is_null(v)) { 2130
Value cur = v; 2131
while (sigil_is_pair(cur)) {−2106
n = flatten_floats(sigil_car(cur), out, cap, n);+2132
n = flatten_floats(sigil_car(cur), out, cap, n, depth + 1); 2133
if (n < 0) return -1; 2134
cur = sigil_cdr(cur); 2135
}@@ -2112,7 +2138,7 @@ static int flatten_floats(Value v, float *out, int cap, int n)
2138
if (sigil_is_vector(v)) { 2139
size_t len = sigil_vector_length(v); 2140
for (size_t i = 0; i < len; i++) {−2115
n = flatten_floats(sigil_vector_ref(v, i), out, cap, n);+2141
n = flatten_floats(sigil_vector_ref(v, i), out, cap, n, depth + 1); 2142
if (n < 0) return -1; 2143
} 2144
return n;@@ -2182,7 +2208,7 @@ static Value native_set_shader_uniform(SigilVM *vm, int argc, Value *args)
2208
} else if (sigil_is_pair(v) || sigil_is_null(v) || sigil_is_vector(v)) { 2209
/* List or vector of numbers, flat or nested per element. */ 2210
float scratch[SIGIL_GFX_UNIFORM_BUFFER_SIZE / sizeof(float)];−2185
int n = flatten_floats(v, scratch, (int)(sizeof(scratch) / sizeof(float)), 0);+2211
int n = flatten_floats(v, scratch, (int)(sizeof(scratch) / sizeof(float)), 0, 0); 2212
if (n < 0) { 2213
sigil__vm_set_error(vm, SIGIL_ERR_TYPE, 2214
"set-shader-uniform: value must contain only numbers (nested lists/vectors allowed)");test/test-shader-limits/src/test-shader-limits/scene.sglmodified
@@ -296,6 +296,11 @@
296
(say "probe" name "FAIL" err) 297
(say "probe" name "ok")))) 298
+299
;; seventeen scalar declarations, one past sokol's member cap+300
(define SEVENTEEN-FLOATS+301
(let loop ((i 0) (acc ""))+302
(if (= i 17) acc (loop (+ i 1) (string-append acc "uniform float f" (number->string i) ";\n")))))+303
304
(define (run-probes!) 305
(expect-error "make-texture-size" 306
(lambda () (make-texture-from-pixels 4 4 (make-bytevector 10 0))))@@ -313,6 +318,16 @@
318
(lambda () (set-shader-uniform *palette* 'levels 0.5))) 319
(expect-error "filter-name" 320
(lambda () (make-render-target 4 4 'cubic)))+321
;; a GLSL error must be an error, not a sokol validation abort (a+322
;; failed sg_make_shader still returns an id)+323
(expect-error "shader-compile-error"+324
(lambda () (load-shader VERT-SOURCE (string-append GLSL-PREFIX "out vec4 fragColor;\nvoid main() { fragColor = ; }\n"))))+325
;; the block caps are errors, not silent drops: 16 mat4 fill the+326
;; 1 KiB exactly, so u_resolution after them does not fit+327
(expect-error "uniform-block-too-large"+328
(lambda () (load-shader VERT-SOURCE (string-append GLSL-PREFIX "uniform mat4 m[16];\nuniform vec2 u_resolution;\nout vec4 fragColor;\nvoid main() { fragColor = m[3][0] * u_resolution.x; }\n"))))+329
(expect-error "uniform-too-many"+330
(lambda () (load-shader VERT-SOURCE (string-append GLSL-PREFIX SEVENTEEN-FLOATS "out vec4 fragColor;\nvoid main() { fragColor = vec4(f0 + f16); }\n")))) 331
(expect-ok "uniform-nested" 332
(lambda () (set-shader-uniform *palette* 'palette PALETTE))) 333
(expect-ok "uniform-flat-vector"test/test-shader-limits/verify.mjsmodified
@@ -74,6 +74,7 @@ const PATTERN_B = [
74
[255, 255, 0], [0, 255, 0], [255, 255, 0], [0, 255, 0], 75
]; 76
const PROBES = ["make-texture-size", "update-texture-size", "update-same-frame", "update-immutable",+77
"shader-compile-error", "uniform-block-too-large", "uniform-too-many", 78
"uniform-count", "uniform-scalar-for-array", "filter-name", "uniform-nested", "uniform-flat-vector"]; 79
80
// The seed, as scene.sgl draws it, and a CPU Life on a torus.@@ -206,13 +207,22 @@ function checkReaction(frame) {
207
// sokol logs through slog_func: natively sigil-app's format `[sg][error][id:N] ...` 208
// on stderr, on the web sigil-graphics' own `sokol[level=L] file:line [tag] msg` 209
// on the console (level 0 panic, 1 error, 2 warning, 3 info; the web build−209
// has no SOKOL_DEBUG so the message text is empty and the line number is+210
// has no SOKOL_DEBUG (zig cc defines NDEBUG at -Oz, measured 2026-09-19 with+211
// `zig cc -Oz -target wasm32-wasi -E` on an #ifdef NDEBUG probe) so the+212
// message text is empty and the line number is 213
// the pointer). A render target whose pass sokol refuses draws nothing and 214
// says so only here, so any error-level line is a failure.+215
// The shader-compile-error probe fails one GLSL compile on purpose, which+216
// sokol reports as GL_SHADER_COMPILATION_FAILED (item id 7; sokol_gfx.h:10828+217
// in the vendored header): exactly one such line is expected and exempt.+218
const COMPILE_FAIL = (l) => /\[sg\]\[error\]\[id:7\]/.test(l) || /^sokol\[level=1\] \S+:10828 \[sg\]/.test(l); 219
function checkGlLog(lines) {−213
const bad = lines.filter((l) => /\[(sg|sgp|sapp)\]\[(error|panic)\]/.test(l) || /^sokol\[level=[01]\]/.test(l));+220
const all = lines.filter((l) => /\[(sg|sgp|sapp)\]\[(error|panic)\]/.test(l) || /^sokol\[level=[01]\]/.test(l));+221
const expected = all.filter(COMPILE_FAIL);+222
const bad = all.filter((l) => !COMPILE_FAIL(l)).concat(expected.slice(1));+223
if (expected.length !== 1) bad.push(`expected exactly one deliberate compile failure line, saw ${expected.length}`); 224
const warn = lines.filter((l) => /\[(sg|sgp)\]\[warning\]/.test(l) || /^sokol\[level=2\]/.test(l));−215
if (!bad.length) pass("gl-log", `no sokol error lines${warn.length ? "; " + warn.length + " warning(s): " + warn.slice(0, 3).join(" | ") : ""}`);+225
if (!bad.length) pass("gl-log", `no sokol error lines beyond the one deliberate compile failure${warn.length ? "; " + warn.length + " warning(s): " + warn.slice(0, 3).join(" | ") : ""}`); 226
else fail("gl-log", `${bad.length} sokol error line(s): ${bad.slice(0, 4).join(" | ")}`); 227
} 228