Repository navigation
Give a number the type of the place it is given, and write it as a literal - #361
Open
bigmistqke wants to merge 4 commits into
Open
bigmistqke wants to merge 4 commits into
bigmistqke wants to merge 4 commits into
Conversation
An integer vector constructor given a number beside a node, as `ivec2(i, 0)`, now writes the number as a literal of its component type: `ivec2(_rmsl_u0, 0)` on GLSL, `vec2<i32>(…, 0i)` on WGSL, `a[1] = 0` on JavaScript, and an integer constant on WebAssembly. It built the number as a float and converted it: GLSL wrote `0.0` into the integer constructor, WGSL wrapped it as `i32(0f)`, and JavaScript clamped and truncated the constant at run time on every call. A constructor given only numbers already wrote integer literals, so a number now compiles alike whether or not a node stands beside it. The number goes through the same check as a number in a constructor of numbers alone, so `ivec2(i, 3e9)` is refused where it is written, as `ivec2(3e9, 0)` already was, and `ivec2(i, 2.9)` truncates to 2. Beside a node, a number outside the range used to compile, and each target converted it to an integer in its own way. TSL writes the conversion, `int( 0.0 )`, so this changes the text rmsl writes, not a value the program computes. Canon units: spec-a-number-beside-a-node-in-an-integer-vector-compiles-to-an-integer-literal, spec-a-number-outside-an-integer-type-is-refused-by-its-constructor. Fixes #360
commit: |
… and an index too A JavaScript number has no shader type of its own. The canon now states one rule for it: wherever a program gives a number, it takes the type that place asks for. That is the operand beside it, the target of an `assign`, a constructor's component, the input of an `Fn` with a layout, and `int` for an index. A number the type cannot hold is refused, and so is a fraction for an integer type outside a constructor, which converts it toward zero as converting a float node does. Two places did not follow the rule: - A number given for an input of an `Fn` with a layout was always a `float`, so `next(3)` for an `int` input was refused. It now passes the `int` 3, and a number for a vector input fills every component. A number for a `bool` or matrix input stays refused. A fraction or a number outside the range of an integer input is refused, with an error that names the input. - A number given as an index compiled three ways. A vector's `element` truncated a fraction silently, and a uniform array's and a storage buffer's built it as a float the program converted at run time, as `[int(2.0)]` and `[i32(2f)]`. Every `element` now compiles a number to an `int` literal, `[2]` and `[2i]`, and refuses a fraction or a negative number where it is given. Where rmsl departs from TSL, the canon records an exception: a fraction for an integer is refused where TSL makes it whole or keeps it a float; a number outside an integer's range is refused where TSL changes the operation's type or converts the float; a negative index is refused where TSL reads the first element; and a number index is an `int` where TSL builds a `uint`. The TSL facts behind the rule are corrected and cite the three.js source as permalinks at the release commit and at the `dev` branch. Where the two versions differ, each is a fact of its own: three.js 0.186.1 builds a number beside an operand as a `float` and rounds a number it builds as an integer, and its `dev` branch gives a number the type of the node beside it and truncates. The TSL axiom now says that a behaviour TSL changed between versions gives a port no one meaning to keep, so parity alone does not decide it, and the canon's preamble asks every fact about TSL to cite its source as a permalink. Canon units: spec-a-number-takes-the-type-its-place-asks-for, spec-a-number-for-a-layout-fn-input-takes-the-input-type, spec-a-fraction-or-a-number-out-of-range-for-an-integer-input-is-refused, spec-a-number-index-is-an-int-literal, spec-a-fraction-or-a-negative-number-as-an-index-is-refused, spec-a-call-with-an-argument-of-the-wrong-type-is-refused, spec-a-uniform-array-is-read-by-element, exception-a-fraction-for-an-integer-is-refused-where-tsl-makes-it-whole, exception-a-number-beyond-an-integer-range-is-refused-where-tsl-changes-the-type, exception-a-number-index-is-an-int-where-tsl-builds-a-uint, exception-a-negative-index-is-refused-where-tsl-writes-zero, exception-a-number-beyond-an-integer-range-beside-a-node-in-a-constructor-is-refused, axiom-a-tsl-shader-ports-by-changing-its-import, fact-tsl-turns-a-number-into-a-constant-node, fact-tsl-r186-builds-a-number-beside-an-operand-as-a-float, fact-tsl-r187dev-gives-a-number-the-type-of-the-node-beside-it, fact-tsl-builds-a-number-beside-a-node-in-a-constructor-as-a-float, fact-tsl-r186-rounds-a-number-it-builds-as-an-integer, fact-tsl-r187dev-truncates-a-number-it-builds-as-an-integer, fact-tsl-builds-a-number-for-a-layout-fn-input-as-its-type, fact-tsl-builds-an-index-as-a-uint. See #360, #362
A number with a fraction given where an integer is asked for is now truncated toward zero in every place: beside an integer operand, `i.add(2.9)` adds 2; for an integer input of an `Fn` with a layout, `next(2.9)` passes 2; as an index, `element(1.5)` reads element 1; and as a bound of an integer `Loop`, `Loop(2.9, …)` runs twice. Each of these was refused, while a constructor truncated, so whether a fraction was accepted depended on the kind of place it went. rmsl already truncates every other float it turns into an integer: a float node converted with `int()`, an integer value the host gives, and a float index of a storage element. A number now follows the same rule. TSL's `dev` branch, r187dev, truncates in each of these places too; three.js r186 rounds instead, which the canon records as the departure. A number the integer type cannot hold is still refused, as is a negative number for an unsigned type and a negative index, a fraction above -1 included: no integer stands for them. Canon units: spec-a-number-takes-the-type-its-place-asks-for, spec-a-fraction-beside-an-integer-truncates, spec-a-fraction-for-an-integer-input-truncates, spec-a-number-out-of-range-for-an-integer-input-is-refused, spec-a-fraction-index-truncates, spec-a-negative-index-is-refused, spec-loop-truncates-a-fraction-for-an-integer-bound, exception-a-fraction-for-an-integer-truncates-where-tsl-r186-rounds.
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 JavaScript number given where rmsl expects a node has no shader type of its own, and each place that takes one decided its type differently. With
ianintuniform andvanintvariable:i.add(2), or assigned,v.assign(2)ivec2(3, 0)ivec2(i, 0)ivec2(_rmsl_u0, 0.0)on GLSL,vec2<i32>(…, i32(0f))on WGSLFnwith a layout,next(3)for anintinputelement(2)intliteral for a vector or a matrix. A float for a uniform array or a storage buffer, converted:[int(2.0)]on GLSL,[i32(2f)]or[u32(2f)]on WGSLThe same number compiled to an integer literal in one place, a converted float in another, and was refused in a third.
Fixes #360.
What changes
A number takes the type of the place it is given: the operand beside it, the target of an
assign, a constructor's component, the input of anFnwith a layout, andintfor an index. It compiles to a literal of that type on every target. A fraction given for an integer is truncated toward zero, in every place, as rmsl already converts a float node, an integer value the host gives and a float index of a storage element.A number for a vector input fills every component: for an
Fnscalewhose layout has avec3input and auintinput,scale(2, 3)passesvec3(2)and theuint3. A number for aboolor matrix input is refused, as before.Fractions and what is refused
A fraction for an integer is truncated toward zero wherever it is given:
i.add(2.9)adds 2,next(2.9)passes 2,element(1.5)reads element 1,ivec2(i, 2.9)holds 2, andLoop(2.9, …)runs twice.What no integer stands for is refused:
ivec2(i, 3e9),next(3e9),i.add(3e9).uvec2(u, -1)ornextwith auintinput given -1, as before.element(-1),element(-0.5).Each refusal comes where the program builds the node, with an error that names the number, and the type or the
elementcall it was given to. To fix such a call, give a number inside the type's range.Code that behaves differently
ivec2(i, 3e9)anduvec3(u, 1, 5e9), withuauintuniform, compiled, and now throw.i.add(2.9)beside an integer, andLoop(2.9, …)for an integer index, were refused, and now truncate the fraction.element(-1)of a uniform array or a storage buffer compiled, and now throws. Of a vector it was already refused, as out of range.next(3)andnext(2.9)for anintinput, andscale(2, 3)for avec3input, were refused and now compile.What TSL does
On TSL's
devbranch, which three.js releases next, a plain number takes the type of the node beside it, and a number for a layoutFn's input takes the input's type. rmsl follows that, and departs from TSL in five places, each recorded inCANON.mdas an exception:devbranch truncates it too, so this departs from three.js r186 only, which rounds it for an input, an index or aLoopbound and keeps it a float beside an operand.devit changes the operation's type, soi.add(3e9)adds two floats.int( 3000000000.0 ), which GLSL leaves undefined and WGSL clamps, so the targets disagree.0uand reads the first element.int, the typeelementgives an index node. TSL builds auint,[ 2u ]. The value is the same for every index an array holds, so only the generated text differs.Facts about TSL
Where three.js 0.186.1 and TSL's
devbranch differ,CANON.mdstates each version as a fact of its own, such asfact-tsl-r186-rounds-a-number-it-builds-as-an-integerandfact-tsl-r187dev-truncates-a-number-it-builds-as-an-integer, each with permalinks to the lines that do it. The TSL-parity axiom now says that a behaviour TSL changed between versions gives a TSL shader no one meaning a port could keep, so parity alone does not decide it; the fraction and out-of-range exceptions cite the fact of each version for that reason. In 0.186.1 a number beside an operand is a float constant, soi.add(2)writesi + 2.0, and a number TSL builds as an integer, for an input or an index, is rounded:generateConstin 0.186.1 writes it withMath.round. Ondeva number takes its neighbour's type and is truncated:generateConstondevwrites it withMath.trunc.Evidence
CANON.md:spec-a-number-takes-the-type-its-place-asks-forstates the rule. Its leaves are the operand specs, the constructor spec, and specs for a layoutFn's input and for an index, with the five exceptions beside them.src/canon/targets.test.tsandsrc/canon/layout-fn.test.ts: a test for each leaf and exception. The tests of a compiled number check the GLSL and WGSL text. Those that compute a value run on JavaScript, WebAssembly, GLSL in WebGL 2 and WGSL on WebGPU, and fail unless all four give the same value. Every new test fails onmain, except the test of an operand out of range, which pins a refusalmainalready made.src/usage.test.ts: two tests asserted the run-time conversion of a uniform array's index,[int(2.0)]and[i32(…)], and now assert the literal.pnpm test: 3314 passed, 11 expected failures, 56 skipped. The expected failures are the tests marked to fail that pin known bugs already onmain.pnpm type-check,pnpm type-check:demosandpnpm test:typespass. A JS draw allocation test fails now and then under the load of the full suite, unrelated to this change, tracked in The JS draw allocation test fails under the load of the full suite and passes alone #363.Follow-ups
CANON.mdpermalinks like those above, and checks each againstdev.spec-a-number-takes-the-type-its-place-asks-for.