GLSL 셰이더 옮기기

커밋 d894fc0의 AUTHORING.md를 그대로 옮긴 것으로, 본문은 아직 영어입니다. 패키지는 0.1.0에서 쓸 typeshade라는 이름으로 가져옵니다.

GLSL 셰이더 옮기기

§1§11 are organised for someone authoring greenfield. This section answers the question a MIGRATING consumer actually asks: my GLSL does X — what is the DSL spelling, and does it survive on WGSL?

It exists because that lookup failed in practice. A real migration re-solved several problems the DSL already solved, purely because the feature was filed under a name nobody knew to search for. Every row below is a thing that was rebuilt by hand at least once.

GLSL constructDSL spellingWGSL resultNotes
uniform Block { … } (ours)uniformStruct (§4)@group/@binding var<uniform>std140 layout comes from reflect(); never hand-count offsets
uniform float u_x; (host prelude declares it)externVarthe same reference, spelled per targetemits NOTHING; lands in reflect().requires
uniform float u_x; (we declare it, host owns it)hostUniform@group/@binding var<uniform>GLSL emits a LOOSE default-block uniform, not a block; reflect() marks owner: 'host'
a whole BLOCK the host ownshostBlockone @group/@binding var<uniform>glsl: 'loose' flattens it to one uniform per member and rewrites blk.fieldfield ON THE IR; 'std140-block' (default) keeps the block. WGSL keeps the block either way — a host bind group is one unit
a host-provided FUNCTIONexternFnsame call, per-target spellingtyped at the call site; no declaration emitted
#ifdef FEATUREwe decidea builder parameter and a plain if (§11)no preprocessorpreferred: the losing arm is never built, so its bindings are never declared
#ifdef FEATURE — the host decidesvariantFamilyone module per point in the matrixemitGuarded generates the #if ladder for a GLSL host that owns the define; every arm is byte-identical to the standalone variant. For a ladder that goes inside an #include, use emitGuardedFragment — same ladder, preamble returned as data
a variant that changes only a VALUEoverrideConst + overrideValues (§11)override + pipeline constantsdo NOT specialise: that multiplies pipelines for nothing
#include "helper.glsl"emitGlslFragment / emitFragmenta module fragment the host concatenatesreturns preamble as DATA; never strip a header with a regex
a statement-level variant slot inside one modulecomposeModule + placeholder (§1)samestatement slots only — it contributes no consts/structs/bindings, so it is not an #include substitute
precision highp … on one declarationthe precision option on hostUniformn/a (WGSL has no precision qualifiers)the stage preamble is the default; this is for a fragment composed into a host program
usampler2D / isampler2Dtexture2duT / texture2diT (§4)texture_2d<u32> / texture_2d<i32>the sampler precision line is emitted for you
#extension … : requireenables (§10)enable …;fails closed (SD0030) on a backend whose capProfile has no row
comparing two emits after an optimizer passsemanticDiffsamecompares IR + reflection, so folding and renaming do not drown the diff; declare your prod plugins as transforms and their rewrites classify into explained instead of the fail-able buckets (§8)

The builtin-value vocabulary — gl_* name → WGSL id

builtin(name, type)’s vocabulary is WGSL’s, typed as WgslBuiltinName — a gl_* spelling or a typo is a tsc error naming the union. This lookup exists because the reverse direction failed in practice: a GLSL-minded author reached for frag_coord (accepted by the GLSL writer at the time), and the module died only when the WGSL writer ran.

Upgrading from a build where frag_coord emitted fine? It did — on GLSL only. That alias was retired precisely because of the asymmetry: a module authored against it compiled on WebGL2 and failed on WGSL, so the trap only sprang on the target you were less likely to be testing. Both writers now reject it with the same remedy — spell it builtin('position', vec4fT) on the fragment input. The rename is spelling-only: GLSL still reads gl_FragCoord and the emitted bytes do not move.

GLSL globalDSL spellingNotes
gl_Positionbuiltin('position', vec4fT) on the VERTEX outputwrites gl_Position on GLSL
gl_FragCoordbuiltin('position', vec4fT) on a FRAGMENT inputreads gl_FragCoord on GLSL. Mind the y-origin: GL window space is bottom-left, WGSL framebuffer space top-left — flip per target (or derive y-symmetric) before consuming .y
gl_VertexIDbuiltin('vertex_index', u32T)GLSL wraps the read as uint(gl_VertexID) (the DSL types it u32, GLSL’s is int)
gl_InstanceIDbuiltin('instance_index', u32T)same uint() wrap
gl_FrontFacingbuiltin('front_facing', boolT)
gl_FragDepthbuiltin('frag_depth', f32T) as the return attr
gl_PointSize / gl_PointCoord— unsupported on BOTH writersnot a WGSL gap being imposed on GLSL: point sprites have per-vendor size caps, and the map dropped them for instanced quad expansion (map/src/shaders/dsl/point.ts) — the emit error’s remedy says the same
float mod(x, y)the free fn mod() (floor-mod).mod()/% is TRUNC-mod (WGSL semantics) and now spells portably on GLSL too — pick by the semantics you mean on negatives

Targeting WebGL2 only? Nothing above narrows what the GLSL writer can express — the neutral names are spellings, not capabilities, and several of this vocabulary’s rules exist to make WebGL2 output MORE defined (round emits roundEven; float % emits a trunc-mod GLSL ES 3.00 actually compiles). For GLSL-only constructs the neutral surface does not model, rawStmt (§1) accepts a { glsl }-only payload: the GLSL writer splices it verbatim, and if the WGSL writer ever runs on that module it fails CLOSED (SD0030) naming every site to port — the deliberate shape for a consumer who excludes WebGPU today but may not forever.

Does it survive on WGSL?

capabilityMatrix([wgslBackend, glslEs300Backend]) answers that per capability, derived from the backends’ own capProfiles rather than transcribed — so it cannot go stale against them. §10 explains the three support classes it reports; SD0030’s hint points back here when an emit fails closed.

Two honesty notes a reader needs before trusting a row:

  • Support is not reachability. f16, subgroups and multiview have profile rows and emit their directives, and NONE of them can be authored today — there is no f16 scalar, no subgroup intrinsic, and neither num_views nor gl_ViewID_OVR. The matrix reports what the profile says; capability-reachability.test.ts is where the allowlist of known-unreachable caps lives, with a reason each.
  • A missing row is a hard stop, by design. It is not a hint to work around: emit throws rather than writing source the driver would reject.

이 페이지 편집