Repository navigation
Add struct, a type of named members, as TSL's struct does - #359
Open
bigmistqke wants to merge 1 commit into
Open
bigmistqke wants to merge 1 commit into
bigmistqke wants to merge 1 commit into
Conversation
`struct(members, name)` makes a struct type and returns its constructor:
```ts
const BVHNode = struct({ min: "vec3", max: "vec3", left: "int" }, "BVHNode");
const getNode = Fn(([i]) => BVHNode(a.xyz, b.xyz, a.w.toInt())).setLayout({
name: "getNode", type: "BVHNode", inputs: [{ name: "i", type: "int" }],
});
getNode(index).get("max");
```
The constructor takes the members in order or as one object by name, and
gives zero to each member it is not given. A number or an array of
numbers is built as the member's type. `get(name)` reads a member, an
`assign` on a member of a struct variable writes it, and an `assign` on
the variable writes every member. A member may be another struct, named
by its name, and a layout of an `Fn` names a struct as its return type or
an input's type. A struct given no name is named `StructType` and a
number. `serialize` and `deserialize` carry the struct types a graph uses.
GLSL and WGSL declare each struct once, a struct a member names ahead of
the struct that holds it, and call a function with a layout that takes or
returns one. JavaScript and WebAssembly run the body of such a function
where it is called, so no struct crosses a call there. A new pass after
the calls are written out splits each struct variable into a variable for
each member, and each member read into that variable. A whole assignment
from anything but another variable reads every member into a temporary
first, so `v.assign(S(v.get("b"), v.get("a")))` swaps the two members.
A struct stays inside a program. Every operation refuses one, a program
whose result is a struct is refused, an assignment between a struct and
another type is refused, and uniforms, attributes, varyings and storage
buffers refuse a struct type, where they are made and again in a graph
`deserialize` rebuilt.
rmsl departs from TSL where TSL builds a program no target runs or hides
a mistake. It refuses a member the struct lacks, which TSL reads as 0.
It refuses a node of another type than its member, which TSL converts,
as rmsl refuses mixed operands elsewhere. It refuses a program that holds
values of two structs made under one name: TSL declares both under the
one name, which neither GLSL nor WGSL compiles. It refuses `equal` of two structs, which TSL writes
as `==`, defined by GLSL and not by WGSL. It builds an array of numbers
given for a member, and a number given in the object form, as the
member's type, where TSL gives such a member zero. It refuses a storage
buffer of structs, which TSL takes; issue #358 asks whether rmsl should.
`struct` given a name again makes that struct again, and a layout names
the latest, so a module that a development server reloads redefines its
structs. `deserialize` gives a struct that was given no name a new
number, so a graph saved in one process loads in another, and refuses a
named struct whose members differ from the struct of that name in the
loading program.
A struct name is one GLSL and WGSL can declare, which excludes the
structs the WGSL target declares itself, and a variable or a function
named like a struct gets a number appended. A member may take the name
of a built-in function, such as `min` or `max`, as GLSL and WGSL allow:
the list of reserved names now keeps the keywords apart from the
built-ins, adds the words WGSL reserves, and a member name is checked
against those two lists. A variable, an input or a function with a
layout named like a word WGSL reserves, such as `type`, is declared under
a name WGSL can, `type_`, as one named like a keyword already is; WGSL
refuses `var type: f32`.
A TSL shader that groups values in a struct now ports by changing its
import, and a function with a layout can return several values, as a
GLSL or WGSL function does through a struct.
Canon units: spec-a-struct-groups-named-members-as-tsls-struct-does and
the specs and exceptions nested in it, spec-a-taken-function-name-gets-a-number, with the facts
fact-tsl-struct-makes-a-type-of-named-members,
fact-tsl-reads-a-member-a-struct-lacks-as-zero,
fact-tsl-converts-a-value-to-the-type-of-its-member,
fact-tsl-declares-two-structs-of-one-name,
fact-tsl-builds-an-array-or-a-named-number-for-a-member-as-zero,
fact-tsl-takes-a-struct-as-the-element-type-of-a-storage-buffer,
fact-tsl-writes-a-comparison-of-structs-with-the-operator and
fact-wgsl-compares-no-structs.
See #358
commit: |
This branch has not been deployed
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.
Problem
A GLSL or WGSL function returns one value. To return several, such as the bounds and the child index of a node of a bounding volume hierarchy read from a texture, a shader declares a struct and returns that. rmsl has no struct type, so a function with a layout (an
Fngiven{ name, type, inputs }, which GLSL and WGSL emit as a function of its own) cannot return several values, and a shader written against TSL (the shading language of three.js) that uses TSL'sstructdoes not port to rmsl by changing its import.What changes
struct(members, name)makes a struct type and returns its constructor, as TSL'sstructdoes:GLSL declares the struct and the function, and calls it:
WGSL declares
struct BVHNode { min: vec3<f32>, max: vec3<f32>, left: i32 }andfn getNode(i: i32) -> BVHNode. JavaScript and WebAssembly have no struct type, so each member of a struct variable becomes a variable of its own: in the emitted JavaScript,toVar("node")givesnode_min,node_maxandnode_left. A name taken twice gets a number appended, as for any variable.What a struct does:
BVHNode(min, max, left), or as one object,BVHNode({ min, max, left }). A member it is not given is zero, soBVHNode()is all zeros.BVHNode(1, [0, 1, 2], 3),1givesvec3(1),[0, 1, 2]givesvec3(0, 1, 2)and3givesint(3).value.get(name)reads a member. On a struct variable, made withtoVar(),get(name).assign(x)writes that member, andassign(value)writes every member. A whole assignment reads its value in full before it writes a member, sov.assign(S(v.get("b"), v.get("a")))swaps two members.outer.get("inner").get("a").typeor an input'stype.StructTypeand a number, such asStructType0. A graphdeserializeloads gives such a struct a new number, so a graph saved in one process loads in another.VertexInput,VertexOutputandFragmentOutput, which the WGSL target declares itself. A variable or anFnwith a layout named like a struct gets a number appended, sotoVar("Light")for aLightstruct declaresLight1.structgiven a name again makes that struct again, so a module that a development server reloads redefines its structs, and a layout names the latest. A program that holds a value of each version is refused as it compiles.deserializerefuses a graph whose named struct has other members than the struct of that name in the loading program.minormax. A member named like a keyword or a reserved word of GLSL or WGSL, such astype, or with the prefixgl_or_rmsl_or two underscores in a row, is refused.serializeanddeserializecarry the struct types a graph uses.structreturns aStructConstructor, a value of it is aStructNode, andtoVar()gives aStructVar, whosegetreturns a variable thatassignwrites.getof a name the struct lacks, and a value of another type for a member, are type errors. A member that is itself a struct is typedany: its type is given as a string, such as"Inner", which TypeScript cannot resolve back to that struct's members.A variable, an input or an
Fnwith a layout named like a word WGSL reserves, such astypeortarget, is declared under a name WGSL can,type_, as one named like a keyword already is. WGSL refusesvar type: f32, so a program with such a variable did not compile on WGSL; it now does.A struct stays inside a program, the node graph a compile function is given:
equal,add,selectand every other operation refuse a struct. Compare or combine the members.Fninside the program may return a struct.uniform,uniformArray,attribute,varying,attributeArray,instancedArrayandstoragerefuse a struct type. Give each member an input of its own.Where rmsl departs from TSL
TSL code that rmsl refuses, with an error that names the cause:
getof a member the struct lacks. TSL logs a warning and reads 0.floatfor avec3member or afloatfor anintmember. TSL converts it. rmsl refuses mixed types here as it does for the operands of an operation, and the error names the conversion, such astoInt().equalof two structs. TSL writes it as==, which GLSL defines for structs and WGSL does not, so the same program would compile on one target only.TSL code that rmsl builds differently: an array of numbers given for a member, and a number given in the object form,
BVHNode({ max: 4 }), are built as the member's type. TSL gives zero to such a member, so a ported shader that relied on that zero now reads the value it wrote.Evidence
src/canon/struct.test.tsandsrc/canon/struct.test-d.ts: a test for each behaviour and refusal above. The tests that compute a value run on all four targets and assert that they agree: GLSL in WebGL 2 in a browser, WGSL on WebGPU through Dawn, JavaScript and WebAssembly. Every GLSL and WGSL shader the tests compile is checked by the browser's WebGL 2 compiler and by Dawn.devbranch.CANON.mdrecords each behaviour as a spec, each departure from TSL as an exception that cites the TSL fact it departs from, and those facts.pnpm test: 3334 passed, 11 expected failures, 56 skipped. The expected failures are the tests marked to fail that pin known bugs already onmain. None comes from this change.pnpm type-check,pnpm type-check:demosandpnpm test:typespass.