Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 53 additions & 27 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ pub fn build(b: *std.Build) void {
// these defaults, so a mismatch fails `labelle build` before any user code.
// Set verified end-to-end STANDALONE (init → install → generate → full
// compile against the published release tarballs, scratch LABELLE_HOME,
// no sibling checkouts) for core 1.32.0 / engine 2.12.2 / gfx 1.30.1 with
// cli 1.61.2, on the bgfx_desktop, null_desktop, raylib_desktop and
// sokol_desktop targets. engine/gfx/cli are exactly `labelle-cli` v1.61.2's
// `versions.zon`, the set its own `labelle upgrade all` writes; core is
// AHEAD of it (that file still says 1.28.0 through cli v1.67.0) — see the
// bgfx paragraph below for why, and bump the cli side to match. Keep the
// two in step: when they drift, `init` scaffolds a project the very next
// `upgrade` rewrites.
// no sibling checkouts) for core 2.0.0 / engine 3.0.0 / gfx 2.0.0 with
// labelle-bgfx 0.21.0 on the bgfx_desktop target (PR #733), after the
// 1.32.0 / 2.12.2 / 1.30.1 predecessor was verified the same way on
// bgfx/null/raylib/sokol (#736). labelle-cli's `versions.zon` (the set
// its `labelle upgrade all` writes) still says 1.28.0 / 2.12.2 / 1.30.1
// through cli v1.67.0 — BEHIND this trio; bump the cli side to match.
// Keep the two in step: when they drift, `init` scaffolds a project the
// very next `upgrade` rewrites.
//
// The hard floor inside the trio (#679): gfx >= 1.30.0 re-exports
// `core.TextureId` instead of declaring its own (labelle-gfx#328,
Expand Down Expand Up @@ -56,29 +56,31 @@ pub fn build(b: *std.Build) void {
// (v0.96.0 bumped engine 2.7.0 → 2.11.0 while leaving core at 1.26.0, so
// every fresh `labelle init` scaffold failed to compile.)
//
// core 1.32.0 (#731 follow-up): the builtin `.bgfx` provider default
// (`src/config.zig` builtinProvider) is labelle-bgfx 0.20.0, the lowest
// bgfx that survives core v1.32.0's `MaterialEffect.pixel_water`. Its
// HARD core floor is 1.28.0 (`core.BackendTextureId`, a struct field
// type, bgfx >= 0.15.0) — 0.20.0 does compile against core 1.28.0,
// verified standalone; the v1.32.0 `PixelWaterDraw` / `PIXEL_WATER_*`
// names it takes from `backend_contract` are reached only through
// `@hasField(MaterialEffect, "pixel_water")`-gated paths or lazy
// top-level aliases. 1.32.0 is a CURATED floor like the gfx 1.28 one: it
// is the core bgfx 0.20.0 was released against (its own build.zig.zon),
// the only core on which the pixel_water effect the default backend
// ships is reachable, and the pairing labelle-bgfx's own examples run
// on. A `labelle init --backend=bgfx` scaffold should not pair a backend
// with a core older than the backend's own pin. `src/init_cmd.zig`'s
// "scaffold core default pairs with the builtin bgfx provider" test
// asserts both floors.
// core 2.0.0 / gfx 2.0.0 / engine 3.0.0 (PR #733, game-owned shader
// materials): the builtin `.bgfx` provider default (`src/config.zig`
// builtinProvider) is labelle-bgfx 0.21.0, the first contract-v2 backend.
// It names core's `shader_material` contract UNGATED and its
// `MATERIAL_CONTRACT_VERSION == 2`, both of which only core >= 2.0.0
// declares — so its HARD core floor is 2.0.0 (a compile break inside the
// backend on any older core), and the assembler's generated materials
// module `@compileError`s on any core below it. gfx 2.0.0 and engine
// 3.0.0 are the releases built against that core (their build.zig.zon
// pins: gfx → core 2.0.0, engine → gfx 2.0.0); the specialized
// PixelWater contract the 1.32.0 core carried is GONE in 2.0.0, so the
// 1.x/2.12.x/1.30.x trio and bgfx 0.20.0 do not mix with any of these.
// The four move TOGETHER (the #731 lesson: bumping the provider without
// the core, or vice versa, ships a default that fails at the first
// `labelle build`). `src/init_cmd.zig`'s `bgfx_core_floors` carries the
// 0.21.0 → 2.0.0 floor and its "scaffold core default pairs with the
// builtin bgfx provider" test asserts the pairing; `checkTrioFloors`
// asserts the trio.
// Bump all three together when moving the engine default (their
// build.zig.zon pins/floors must agree — read them from the tags), and
// keep `src/init_cmd.zig`'s "scaffold pins a MUTUALLY COMPATIBLE trio"
// test satisfied — it encodes the floors below as assertions.
const core_version: []const u8 = b.option([]const u8, "core_version", "Default core library version") orelse "1.32.0";
const engine_version: []const u8 = b.option([]const u8, "engine_version", "Default engine library version") orelse "2.12.2";
const gfx_version: []const u8 = b.option([]const u8, "gfx_version", "Default gfx library version") orelse "1.30.1";
const core_version: []const u8 = b.option([]const u8, "core_version", "Default core library version") orelse "2.0.0";
const engine_version: []const u8 = b.option([]const u8, "engine_version", "Default engine library version") orelse "3.0.0";
const gfx_version: []const u8 = b.option([]const u8, "gfx_version", "Default gfx library version") orelse "2.0.0";
// Version this assembler binary stamps into a freshly scaffolded
// project.labelle's `assembler_version` field.
//
Expand Down Expand Up @@ -181,6 +183,29 @@ pub fn build(b: *std.Build) void {
bin_tests.root_module.link_libc = true; // see assembler_exe comment above
test_step.dependOn(&b.addRunArtifact(bin_tests).step);

const material_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize }), .filters = &.{ "material_schema", "material_pipeline", "component_collisions" } });
material_tests.root_module.addOptions("build_options", options);
material_tests.root_module.addImport("flow_codegen", flow_codegen_module);
material_tests.root_module.link_libc = true;
const material_test_run = b.addRunArtifact(material_tests);
const material_test_step = b.step("test-materials", "Validate material JSON, descriptor generation and build wiring");
material_test_step.dependOn(&material_test_run.step);
test_step.dependOn(&material_test_run.step);
const shaderc_test_exe = b.option([]const u8, "shaderc", "Host shaderc for material integration tests") orelse b.graph.environ_map.get("LABELLE_SHADERC");
const material_test_core = b.option([]const u8, "material-test-core", "Local core checkout for material integration test");
const material_test_zbgfx = b.option([]const u8, "material-test-zbgfx", "Pinned zbgfx directory for material integration test");
const material_integration = b.step("test-material-integration", "Compile shaders/descriptors and verify include cache invalidation with a real shaderc");
if (shaderc_test_exe != null and material_test_core != null and material_test_zbgfx != null) {
const python = b.option([]const u8, "python", "Python executable for integration tests") orelse (if (@import("builtin").os.tag == .windows) "py" else "python3");
const integration = b.addSystemCommand(&.{python});
if (@import("builtin").os.tag == .windows and std.mem.eql(u8, python, "py")) integration.addArg("-3");
integration.addFileArg(b.path("test/material_pipeline_e2e.py"));
integration.addArgs(&.{ "--zig", b.graph.zig_exe, "--shaderc", shaderc_test_exe.?, "--core", material_test_core.?, "--zbgfx", material_test_zbgfx.? });
material_integration.dependOn(&integration.step);
} else {
material_integration.dependOn(&b.addFail("Supply -Dshaderc=PATH (or LABELLE_SHADERC), -Dmaterial-test-core=DIR and -Dmaterial-test-zbgfx=DIR").step);
}

// ── `test-cache`: the local-slot cache machinery, alone ─────────────
//
// The Windows CI job runs THIS, not `test`. `zig build test` on Windows
Expand Down Expand Up @@ -403,6 +428,7 @@ pub fn build(b: *std.Build) void {
});
const run_test = b.addRunArtifact(t);
test_step.dependOn(&run_test.step);
if (std.mem.eql(u8, test_file, "test/build_zig_tests.zig") or std.mem.eql(u8, test_file, "test/build_zig_zon_tests.zig")) material_test_step.dependOn(&run_test.step);
if (std.mem.eql(u8, test_file, "test/animation_assets_tests.zig")) {
b.step("test-animation", "Test JSONC animation asset wiring").dependOn(&run_test.step);
}
Expand Down
67 changes: 67 additions & 0 deletions docs/shader-materials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Game-owned shader materials

Place each descriptor at `materials/<name>/material.json`. Generate with the bgfx backend (the null tests target is also supported). No water/fog/lamp semantics or ECS components are generated by this pipeline.

```json
{
"version": 1,
"label": "fog",
"fragment": "fs_fog.sc",
"targets": ["spv", "glsl", "essl", "mtl"],
"parameters": [{"name": "u_tint", "kind": "vec4", "defaults": [1,1,1,1]}],
"textures": [{"name": "s_noise", "sampler": "linear"}],
"blend": "alpha"
}
```

`version`, `fragment`, and nonempty `targets` are required. Unknown fields are rejected. Label defaults to the folder name. Parameters/textures default empty, count defaults 1, defaults default empty (core zero-initializes), sampler defaults `point`, blend defaults `alpha`. Kinds: `scalar`, `vec2`, `vec3`, `vec4`, `mat4`; blend: `alpha`, `additive`. Nonempty defaults must have exactly channels × count finite floats. The limits match core contract v2: 16 parameters, 4 extra textures, 64 registers including one reserved for the automatic `u_material_rect`. Matrices use four registers each. Names are ASCII identifiers, maximum 63 bytes. All bindings must be unique, including across parameters and textures. `s_tex`, `u_material_rect`, and BGFX predefined `u_view*`, `u_proj`, `u_inv*`, `u_model*`, `u_alphaRef4` names listed in `material_schema.zig` are reserved.

Folder names are identifiers and must be unique ignoring case. Generated symbols `core`, `sm`, `parameters`, `textures`, `shaders`, `label`, `blend`, `descriptor`, `bindings`, `_` are reserved folder names.

Fragments pair with the existing sprite vertex shader. Inputs are `v_color0` and `v_texcoord0`; the build supplies its fixed sprite `varying.def.sc`. The base sprite texture is `s_tex` at slot 0. Additional textures occupy slots 1 onward in descriptor order. `u_material_rect` is supplied per draw. Scalars/vectors follow core's parameter packing; shader code must use the corresponding BGFX-compatible uniform declarations. There is no custom vertex shader option.

```zig
const materials = @import("materials");
// Preferred engine API: use catalog-backed engine texture bindings.
try game.createShaderMaterial(entity, .{
.label = materials.fog.label,
.shaders = materials.fog.shaders,
.parameters = &materials.fog.parameters,
.blend = materials.fog.blend,
// .textures = caller's engine.ShaderTextureBinding slice
});
```

### Texture bindings are catalog `.image` entries only

The engine resolves a `.catalog` binding through the AssetCatalog and rejects
any entry whose `loader_kind` is not `.image` with `error.InvalidTexture`
(`labelle-engine/src/game/shader_material_mixin.zig`). **An atlas frame is not
an image entry**, so a mask/lookup texture a fragment shader samples must be
declared as a STANDALONE `.{ .name = ..., .image = "..." }` resource in
`project.labelle`, not packed into a sprite atlas. The generic material path
carries the same constraint the retired built-in water effect had; budget the
extra standalone PNGs when migrating.

A binding that is registered but not yet resident returns `error.TextureNotReady`
(streaming) or `error.AssetLoadFailed`; retry, do not treat either as fatal.
Textures are sampled in the sprite's own UV space, so a mask normally spans
exactly the logical rectangle it describes.

The module imports the same `labelle-core` instance as the game. It is wired into ordinary and promoted scripts, pack modules, executables, and test roots. It has no backend imports. For lower-level use, `var bindings = materials.fog.textures; bindings[0].texture = backend_id; const d = materials.fog.descriptor(&bindings);` returns a **borrowed** core descriptor; retain the caller's array through creation. The engine uses a distinct texture union for catalog pinning, so compose its descriptor from the exported fields instead of passing the core helper result.

| Variant | shaderc platform | profile |
|---|---|---|
| spv | linux | spirv |
| glsl | linux | 120 |
| essl | android | 300_es |
| mtl | osx | metal |
| dx11 | windows | s_5_0 |

Every requested variant is compiled with `--type fragment -O 3`. Desktop Windows/Linux require `spv` for the initial Vulkan path; Apple targets require `mtl`; Android/web require `essl`. Absent variants remain empty; no silent fallback or old binary reuse occurs on compilation failure. DX11 is schema/build supported but requires the pinned compiler's DirectX runtime support; it is not part of the four-variant acceptance fixture.

Generated `build.zig.zon` adds a separate hash-pinned `material_shaderc` host dependency; framework release pins are unchanged. The host compiler is built automatically by Zig. Set `LABELLE_SHADERC` or `-Dshaderc=<absolute executable>` to reuse a compatible host compiler. The pinned package's `shaders/` includes are always used, even with an executable override. Cross-target games still execute the host tool.

Shader source paths and literal include paths cannot contain parent traversal, absolute paths, Windows drives/backslashes, or NUL. Macro-based include names and symlinks within the source tree are rejected. Includes may use material-local files or the shared tree, e.g. `#include <shared/lamp_footprint.sc>` from `materials/shared/lamp_footprint.sc`. Keep image assets in `assets/`; material source files are limited to 1 MiB each. All material files plus pinned shader headers are tracked for compilation cache invalidation, conservatively rebuilding all variants on any material-tree edit. Compiler executable/artifact, arguments, source, fixed varyings and includes participate in Zig caching. Outputs live in Zig's cache, never the source tree. Adding/removing descriptor folders requires regeneration; edits to existing descriptors are embedded build inputs and need only `zig build`.

Verification: `zig build test-materials`; real compiler and generated-source coverage: `zig build test-material-integration -Dshaderc=<exe> -Dmaterial-test-core=<core checkout> -Dmaterial-test-zbgfx=<pinned zbgfx directory>`. The integration step checks all four variants, descriptor compilation against real core, cache hits, include edits, compiler failures, escaping includes, duplicate registrations, and missing required variants.
86 changes: 86 additions & 0 deletions docs/shader-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Migrating to game-owned effects

The shader implementation is a coordinated change across core, gfx, engine,
bgfx, assembler and CLI. The toolkit side has shipped: core 2.0.0, gfx 2.0.0,
engine 3.0.0 and bgfx 0.21.0 are released, and `examples/condenser` pins those
releases (no local sibling checkouts). Older releases do not carry the generic
material contract (v2); a game that adds `materials/` must resolve those
versions or newer.

## Ownership

- Core defines the typed material contract and opaque runtime handles.
- Gfx resolves texture IDs and dispatches generic material draws.
- Engine owns entity bindings, catalog pins and cleanup across scene/GPU lifetimes.
- BGFX owns GPU programs, uniforms, instance values and resource validation.
- Assembler discovers material definitions, compiles variants and generates imports.
- CLI validates an explicitly configured host shader compiler.
- The game owns shader sources, components, prefabs, simulation and effect controls.

After this shared infrastructure is released, adding another sprite effect is a
game change: add its material definition/source, component and binding system.
No water, fog or lamp effect registration is required in the toolkit repositories.

## Breaking changes

The specialized PixelWater contract, facade, engine simulation/authoring and
BGFX implementation have been removed. Existing games must migrate their water
state and update logic into game components, as demonstrated by `WaterShader`
in `examples/condenser`. The generic material ABI is version 2; rebuild the
coordinated packages together. Do not mix old compiled material layouts.

`WaterShader`, `FogShader` and `LampShader` are ordinary game component names.
Their shader descriptors live separately under `materials/<name>/material.json`.
Shader source is BGFX shader language; JSON specifies its interface and targets.
Runtime handles must not be authored in prefabs or serialized as durable state.

## Release sequence

1. Release the new core contract.
2. Update the BGFX core dependency pin and release the generic backend and gfx.
3. Release engine's generic entity material API.
4. Release assembler's shader pipeline and CLI integration.
5. Bump the bgfx entry of `ProjectConfig.builtinProvider` (`src/config.zig`) to
the released contract-v2 backend, in the SAME change that adopts the new
core (done: bgfx 0.21.0 with the core 2.0.0 / gfx 2.0.0 / engine 3.0.0
scaffold trio in `build.zig`). `.backend = .bgfx` with no
`.backend_package` resolves through that default, so bumping either side
alone ships a default that fails at the first `labelle build` — the
failure mode assembler#731 repaired in the other direction.
`src/init_cmd.zig`'s `bgfx_core_floors` carries the bgfx >= 0.21.0 →
core >= 2.0.0 hard floor, and `material_pipeline` rejects an explicit
pre-contract-v2 `.backend_package` / `.core_version` pin at generate time
for a project that owns `materials/`.
6. Replace the example's local dependency selections with those released
versions (done: `examples/condenser` pins core 2.0.0, gfx 2.0.0, engine
3.0.0 and bgfx 0.21.0), and add it to the examples-integration lane
(assembler#732: that lane only GENERATES bgfx examples today, so a compile
break in a pinned backend passes unnoticed — build it, do not only
generate it).

All six steps have shipped (core v2.0.0, gfx v2.0.0, engine v3.0.0, bgfx
v0.21.0; assembler defaults in PR #733). A fresh `labelle init --backend=bgfx`
resolves the contract-v2 stack and can own `materials/` with no explicit pins.
To develop against unreleased sibling checkouts, switch the example's four pins
back to `local:../../../labelle-*`. Standalone BGFX tests require
`-Dcore-source=<core>/src/root.zig`.

## Validation

The condenser compiles water, fog, lamp and mist to SPIR-V, GLSL, ESSL and
Metal. Windows/Vulkan execution covers the deterministic scene scenarios in the
example README (16 native scenarios, including per-instance isolation and
independent effect disabling). Its generated unit suite passes 20 tests. See the
example README for controls and reproduction.

Verified live against the released pins:

- macOS/Metal: standalone `labelle run` of the condenser, four materials live
(2/2 instances each), no degrade lines.
- Android/OpenGL ES: Galaxy Tab A7 (Adreno 610) through the CLI path, all eight
material instances live at 62 fps.

Core, gfx, BGFX, assembler and CLI suites and engine's focused shader regression
suite pass. The broad engine suite encounters Windows preview/network and time
API failures; it is not reported as passing. Browser (wasm) execution has not
been verified. Direct3D runtime materials are unsupported.
3 changes: 3 additions & 0 deletions examples/condenser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.labelle/
.test-output/
__pycache__/
Loading
Loading