Rendered from AUTHORING.md at commit d894fc0. The package is imported here by its 0.1.0 name, typeshade.
Capabilities & extensions
A module declares the GPU features its emit needs, by neutral id — never a raw
EXT_* / OVR_* string:
module({ enables: ['floatRenderTarget'], funcs: [vs, fs] })Each id folds into the capability gate, so a backend that cannot spell it fails
closed (UnsupportedFeatureError / SD0030, message naming the cap) instead of
emitting source the driver rejects. Resource caps (storageBuffer, compute,
msaaTextureLoad) are DERIVED from the module’s shape and are never declared here —
enables is typed readonly DeclarableCapability[] (Capability minus those three), so
naming one is a compile error, not a silent no-op.
Each backend owns ONE capProfile table — neutral id → { directive?, hostFeature? } —
and that table is the single authority: coverage is built from its keys, the directive
header from its rows’ directives, and the host-activation list from their
hostFeatures.
enables id | WebGL2 / GLSL ES 3.00 | WebGPU / WGSL |
|---|---|---|
floatRenderTarget | host: EXT_color_buffer_float | core (nothing to request) |
float32Blend | host: EXT_float_blend | host: float32-blendable |
float32Filterable | host: OES_texture_float_linear | host: float32-filterable |
multiview | source: #extension GL_OVR_multiview2 : require and host: OVR_multiview2 | unsupported (no OVR equivalent) — fails closed |
f16 | unsupported (no GLSL ES 3.00 counterpart) | source: enable f16; and host: shader-f16 |
subgroups | unsupported (no GLSL ES 3.00 counterpart) | source: enable subgroups; and host: subgroups |
A cap may need either or both halves, so a cell lists every half that target needs.
host: the HOST must activate it before pipeline creation. source: the backend
emits the directive itself (#extension right after #version 300 es, enable ahead of
the declarations), deduped + sorted. A cap with a host half and no source half is
byte-neutral: declaring it moves not one emitted byte. The 32 in float32Blend /
float32Filterable is not decoration — both underlying features are 32F-only, while
EXT_color_buffer_float covers 16F and 32F, which is why floatRenderTarget carries
no bitwidth.
A cap can also IMPLY another: float32Blend pulls in floatRenderTarget, because
blending into a float target needs that target to be color-renderable first (with only
EXT_float_blend the FBO comes back INCOMPLETE). reflect().requiredFeatures reports the
closure, so a module declaring one gets both.
multiview caveat: the cap buys the directive only. The DSL cannot yet spell
layout(num_views = N) in; or read gl_ViewID_OVR, so a module declaring multiview
emits the #extension line and still renders single-view. It exists to prove the
#extension mechanism end to end; real multiview authoring is a follow-up.
Activation authority — verify at boot, never at pipeline time
Declaring a cap does not activate anything. On this repo’s runtimes the device decides, at device-creation time, and it is already done by the time a module is emitted:
- WebGL2 — the device constructor (
rhi-webgl2/src/rhi-webgl2.ts) alreadygetExtensions the float pair (EXT_color_buffer_float+EXT_float_blend) when both are present. - WebGPU —
requiredFeaturesare fixed atrequestDevice(rhi-webgpu/src/gpu.ts). A feature not requested there can never be added later; asking at pipeline-creation time is too late.
So a module author’s job is to verify, not to request: check that the booted device covers what the module needs, and fail loud if it does not.
The requirement list is reflect().requiredFeatures — always present, empty when the
module needs nothing, covering the derived caps and the implied ones too. The ids are
neutral because reflection is target-neutral, so translate through the target’s profile
with hostFeaturesFor, THE host-activation lookup (it skips caps with no host half, so
there are no undefined holes to hand a driver):
import { hostFeaturesFor, reflect, glslEs300Backend, wgslBackend } from 'typeshade'
// WebGL2 — verify the already-booted context has each extension.for (const ext of hostFeaturesFor(glslEs300Backend, reflect(m).requiredFeatures)) { if (!gl.getExtension(ext)) throw new Error(`WebGL2 lacks ${ext}`)}
// WebGPU — feed the SAME lookup into requestDevice, at boot.const device = await adapter.requestDevice({ requiredFeatures: hostFeaturesFor(wgslBackend, reflect(m).requiredFeatures),})Compute on WebGL2 — the portable kernel tier
A stage: 'compute' entry declared portable: true is guaranteed to emit on both
backends: natively as @compute on WGSL (zero byte change — portable is not a WGSL
attribute), and on GLSL ES 3.00 through the compute→fragment-GPGPU lowering
(lowerComputeToFragment) run with no emit option. In exchange the kernel must stay
inside the gather-only tier:
const kernel = fn( 'eval_field', { gid: builtin('global_invocation_id', vec3uT) }, ({ gid }) => { const fid = gid.x // … reads only, one write … outColor.at(fid).assign(pack4x8unorm(color)) }, { stage: 'compute', workgroupSize: 64, portable: true },)-
global_invocation_idused only as.x(1-D linear index). -
Exactly one
read_writestorage binding, elementarray<u32>, written exactly once, at indexgid.x— any scatter write, a second write, or zero writes fails. -
A first
uniformbinding of typevec4<u32>— the dispatch uniform:field meaning .xinvocation count .youtput-grid width (W_out) .zunused (reserved) .wunused (reserved) -
No
rawstatements anywhere the entry’s call graph can reach (a per-target escape hatch contradicts the portability claim).
Anything outside that shape fails validation at every emit, on both writers, with
SD0111 and a per-violation remedy — declaring portable without stage: 'compute' fails
at build time with SD0110. analyzePortableKernel (core/passes/portable-kernel.ts) is
the single authority for the shape; the lint rule portable-kernel runs it at every
validate().
Host contract: the WebGL2 lowering changes how the kernel is dispatched, not just how
it is emitted — the host must submit a fullscreen draw into an R32UI target instead of
a compute dispatch. rhi-webgl2/src/compute-webgl2.ts already absorbs this difference, so a
kernel author does not choose it per call site; declaring portable is what lets the
WebGL2 RHI recognize the kernel as eligible for that path.
Outside the tier: barriers, workgroup memory, atomics, scatter writes, and multi-output
kernels are not in v1 — none of those are authorable in the DSL today except via the shapes
SD0111 already rejects. A kernel that needs one of them stays WebGPU-only (omit
portable), or is restructured into multiple gather-only passes.
emulateCompute is deprecated in favor of this tier: pass portable: true at the
authoring site instead of emulateCompute: true at the emit call site. The flag still works,
unchanged, as the synonym for undeclared kernels — nothing that passes it today has to
change.