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