Repository navigation
Step 6, the scene: per-camera and per-world blocks, the scene's shaders quoted - #838
Merged
Merged
Conversation
capture_field now places a fixed-size array of structs, and the new
capture_element_field places each field of its element, with the
compiler's offsetof and sizeof:
_l = gpu.capture_field(s, "world", "lights", offsetof(World, lights))
_c = gpu.capture_element_field(s, "world", "lights", sizeof(Light), "colour", offsetof(Light, colour))
The element is a GLSL struct of its own. It is declared once, before the
struct that holds the array. It has every field that the tree reads of a
value of that type, or sets in a literal of it, in the cell's order. An
element can be handed to a helper whole, and a literal of the type is the
same struct, so its GLSL constructor takes the fields in that order.
The rest follows from that. std140 aligns an element to sixteen and rounds
its size up to sixteen, which is glslang's arrayStride. params_write
repacks each element's fields from the cell, and set_field_words reads
them back from a block's words. OpenGL sets the struct a value at a time
("world.lights[3].colour"). The CPU reads an element as a struct value.
The generator holds each element to glslang's topLevelArrayStride.
The following are refused:
- an element field that is read or set but not placed;
- an array of structs with none of its fields placed;
- a stride that disagrees with the one placed;
- a field placed twice;
- an element field asked of a field that is not an array of structs.
tests/test_gpu_struct_arrays covers this with a Sky of four Lamps. A
Lamp's cell is 64 bytes and its std140 element is 48. Every offset is
held to glslang's reflection, the CPU is held to the same body run as
plain Aether, and each refusal is checked. Six negative controls fail it:
no rounding to sixteen, the cell's stride used in the block, the CPU
reading element 0, the element declared twice, set_field_words ignoring
the element, and the refusal removed. No shader changes:
generate_shaders --check is unchanged, and the other GPU-free gpu suites
pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elpers read in place) The scene's pixel and vertex shaders (FRAGMENT_DEFAULT, VERTEX_DEFAULT, VERTEX_DEPTH, FRAGMENT_DEPTH) need more of ae3d.gpu than the passes quoted so far. This adds it, apart from any shader. The quoted scene comes in a later commit of this stack. Layouts. A layout is a uniform block's structs in order, each field typed and placed in its struct's cell with offsetof. gpu.layout_* builds one, and use_layout(s, set, l) gives it to a shader. The shader declares that set's block with each struct it captures whole, every field read or not. Each entry it does not capture, before the last one it does, becomes padding of the entry's size (vec4 name_unread[n]). So a vertex shader and a pixel shader that read other fields lay one buffer out alike. OpenGL declares each captured struct as a uniform of its name, and the declaration is the same in both stages, so link() takes one struct captured by both. Values are named with their set (uniforms_set). block_size and block_write give a block for checks on the CPU. A field read as another type than the layout gives is refused. Places. capture_binding(s, name, set, binding) puts a texture or a storage buffer at a set and binding of its own, where the hand-written shaders had them. A pixel shader may now read a storage buffer, if it is placed. On OpenGL 4.1, which has no storage buffers, the buffer is a texture of rows: vec4s_at_i and floats_at_i read 1024 vec4s or 4096 floats to a row, as the scene's clustered lamps are uploaded there. Read in place. A @Quotable helper only sees what it is handed, while a hand-written helper reads its uniforms and varyings where it uses them. On MoltenVK the two round apart: fast math orders a sum by where its values were loaded (the water's water_below, docs/aether_quote_design.md section 11). read_in_place(s) changes the GLSL for that case. When every call hands a parameter the same capture, field of one, varying, attribute or literal index of one, the parameter is dropped from the GLSL and the helper reads the value in place, as the hand-written one did. This includes the caller's own parameter read in place, found to a fixpoint. hand_on(s, helper, param) keeps one as a parameter where the hand-written helper took it as one. A helper whose own local would hide the value is refused. Such a shader declares its varyings before its helpers. It is opt-in, and the shaders quoted before are written as they were. Also new: - discard(), as GLSL's statement; on the CPU it drops the pixel (cpu_discarded). - cut_fragment, a pixel shader that writes nothing (a shadow map's cut casters). - The derivatives (dfdx, dfdy, fwidth: zero on the CPU) and texture_grad. - int abs, clamp, min, max, uint_of_int, mod, step, ceil, exp2, log, inversesqrt, and sin, pow and abs of vectors. - mul_cols3 (mat3(a, b, c) * v), mat4_rows (transpose(mat4(...))), mul_transpose_m2, lane_at, clip_depth (a clip z as a depth map stores it, each dialect's own), more swizzles, and vec3_of_v2. - On the CPU: an array of a captured struct handed to a helper whole, a storage buffer handed to one, and int and uint arithmetic held exactly and wrapped to 32 bits. Literals are parsed whole, hex too, and an int is negated exactly. A hash's ints are past an f32's 2^24, and the CPU had rounded them. - set_base: a vertex shader drawn after several sets (the crowd's, after the scene's three, in the next commit). tests/test_gpu_layouts holds a small pair to all of it. Its blocks and padding, both stages' declarations, OpenGL's uniforms and rows, its places, its in-place reads and handed parameters, its discards and its cut shader are checked. Every offset glslang reports is held to ours. The CPU is held to the same arithmetic as plain Aether, a hash among it. The refusals are checked: a mistyped field, an unplaced buffer, a shadowed read, a set given twice. Seven negative controls fail it: the padding's size, no reads in place, no hand_on, the CPU ignoring a discard, OpenGL's rows as a sampler1D, a wrong int shift, and no layout taken. test_gpu_crowd's refusal check now names the placement. No shader changes: generate_shaders --check is unchanged, and every GPU-free gpu suite passes, with no leaks in the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orld's Nic's option 1 (2026-10-20): the per-camera data is split from the per-world now, still with one camera, so every frame is the one it was. The scene's one uniform block is three, each in a descriptor set of its own on Vulkan: - set 0, the draw's (13,056 bytes, a dynamic offset a draw): ae3d.shaders.lit's Draw, DrawMaterial, Surface, Cut, Wounds, Damage and Skin, with the draw's colour, normal, scene-depth and damage images; - set 1, the camera's (800 bytes): ae3d.shaders.frame's Eye, ShadowCascades, Grid and Probes (everything that depends on the view: the matrices, the eye, jitter and history, the capture channel, the cascades fitted to the view, the light grid, the probes), with the shadow atlas, the cluster buffers, the lamps' atlas, the occlusion map and the probes' atlases; - set 2, the world's (1,424 bytes): WorldState (lights: WorldLight[16], the clouds, the time, the wetness, the renderer's world settings) and Weather's fog, with the sky light, the BRDF table, the irradiance and the rays' structure. Each flat block keeps its members in its structs' std140 order, so the hand-written shaders and the quoted ones read one staging layout; the generator checks every member's offset against glslang's in every block (554; it had read only each shader's first block's). vkdesc writes the camera and world sets once a frame, at the frame's first bind; a second camera is a second set 1 bound in the first one's place, and nothing else changes (docs/aether_quote_design.md section 11 has the table). Shaders drawn beside the scene take their sets after it (frame.SCENE_SETS): the crowd's rings at 3, or 4 with its storage buffers. frames_against 59 of 59 within bounds; one sample 56 frames byte-identical but the clouds (#793); Khronos validation with sync clean at 1x and 4x. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FRAGMENT_DEFAULT, FRAGMENT_DEPTH and the scene's vertex shaders, quoted, and drawn on both renderers in place of the hand-written ones: - lit_vertex and lit_plain_vertex (the plain pipelines', no skin stream), lit_pixel; lit_depth_vertex, lit_depth_plain_vertex and lit_depth_pixel for the shadow pass. The crowd's vertex shaders are drawn with lit_pixel and lit_depth_pixel. - Each reads the scene's three blocks through gpu.use_layout, whole structs at sets 0, 1 and 2, and the images and buffers at the hand-written bindings; each helper keeps the hand-written shape and reads its uniforms in place, since MoltenVK's fast math rounds by where a value was loaded. - Vulkan: vkpipes builds the scene's and the shadow pass's pipelines from the quoted words; vkframe checks each tree against the hash its words were made from first (vkquote.fresh, exported). - OpenGL: the four scene programs are compiled from the quoted GLSL, and gl_uniform_location finds a value of theirs by the name the quoted shader gives the hand-written uniform it was set by (lit.gl_name). - The crowd's vertex shaders capture the frame's Eye as `viewer`: on OpenGL one name is one uniform across a program, and lit_pixel captures the whole camera as `eye`. - The generator writes the six shaders, and checks every member glslang reports of a shader of layouts is one ae3d.gpu placed (it is handed every field of a struct, and glslang reports the ones read). - The ray-query pixel shader is still the hand-written one. tests/test_gpu_lit: the GLSL in both dialects and compiled; every varying at FRAGMENT_DEFAULT's place, the plain and crowd vertex shaders handing them on there; each value in its block's set; every value one the OpenGL renderer sets by its hand-written name; on the CPU the capture channels' colour, normal and motion, and the cut's far side not drawn. Six negative controls fail it, and two the generator's checks. test_backend_parity at 1x and at 4x: 56 frames byte-identical to the hand-written scene's on Vulkan and OpenGL but the clouds (#793); frames_against 59 of 59 within bounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the quoted scene's passing run, the shaders it replaced go: VERTEX_DEFAULT, VERTEX_DEPTH, FRAGMENT_DEPTH, shaders.default_shader (a model with no shader is drawn by the scene's; only smoke_core called it), and their SPIR-V: scene_vk.vert, scene_plain_vk.vert, scene_vk.frag, depth_vk.vert, depth_plain_vk.vert and depth_vk.frag. FRAGMENT_DEFAULT stays as the ray-query pixel shader's source alone (scene_rq_vk.frag). The generator lays the three blocks out from their tables (ae3d.shaders.lit, ae3d.shaders.frame) instead of from the hand-written declarations, each typed as the ray-query shader declares it where it does; the blocks and ae3d.vkscene come out byte for byte as they were. No scene-block uniform was left unread: each of the 134 is read by a quoted shader, by the ray-query one or by the water's copy, so none is removed. On the stack's head: generate_shaders --check current; frames_against 59 of 59 within bounds against main; test_backend_parity at 1x and 4x byte-identical to main's on both renderers but the clouds (#793); Khronos validation with sync, nothing excluded, clean at 1x and 4x over parity, zombie_city, street_drive and 14 scene suites; ./ci.sh green (the 8 hardware skips). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 6, the scene (docs/aether_quote_design.md section 11), as Nic directed on 2026-10-20: "Scene: option 1. Split per-camera from per-world now, still with one camera, so every frame stays identical. Put the per-camera data in its own block, so a second camera later just means binding a second one. No multi-camera work before the scene."
Five commits, each one step:
Light[16](gpu.capture_element_field), with tests and negative controls.gpu.use_layout: a block of whole structs at a set), images and buffers at given bindings,discard, helpers that read their uniforms in place, exact int and uint arithmetic on the CPU, and the calls the scene uses.tests/test_gpu_layouts(31 checks, 7 negative controls).ae3d.shaders.lit): FRAGMENT_DEFAULT, FRAGMENT_DEPTH and the scene's vertex shaders, drawn on Vulkan and OpenGL.tests/test_gpu_lit(56 checks, 6 negative controls; plus 2 for the generator).default_shaderand six SPIR-V files. FRAGMENT_DEFAULT stays as the ray-query variant's source (see below).Where each field went
Verification (M6, Aether 0.834.0, glslang 16.6.0)
generate_shaders --checkcurrent, rebuilt before each check.For Nic
scene_rq_vk.frag) is still hand-written, built from FRAGMENT_DEFAULT. Quoting it needsaccelerationStructureEXTandrayQueryEXTin ae3d.gpu, and only CI's lavapipe can test it, since no local device here traces rays. Do you want it quoted next, or kept as it is?lit.gl_namemaps it to the quoted shader's name. Custom GLSL shaders keep their uniform names, compatlightPosand the rest included.viewPosfrom the camera set, so it gets this frame's value where it used to get last frame's. Only billboard casters depend on it, and the frames are the same.🤖 Generated with Claude Code