기능과 확장

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

기능과 확장

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 idWebGL2 / GLSL ES 3.00WebGPU / WGSL
floatRenderTargethost: EXT_color_buffer_floatcore (nothing to request)
float32Blendhost: EXT_float_blendhost: float32-blendable
float32Filterablehost: OES_texture_float_linearhost: float32-filterable
multiviewsource: #extension GL_OVR_multiview2 : require and host: OVR_multiview2unsupported (no OVR equivalent) — fails closed
f16unsupported (no GLSL ES 3.00 counterpart)source: enable f16; and host: shader-f16
subgroupsunsupported (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) already getExtensions the float pair (EXT_color_buffer_float + EXT_float_blend) when both are present.
  • WebGPUrequiredFeatures are fixed at requestDevice (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_id used only as .x (1-D linear index).

  • Exactly one read_write storage binding, element array<u32>, written exactly once, at index gid.x — any scatter write, a second write, or zero writes fails.

  • A first uniform binding of type vec4<u32> — the dispatch uniform:

    fieldmeaning
    .xinvocation count
    .youtput-grid width (W_out)
    .zunused (reserved)
    .wunused (reserved)
  • No raw statements 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.

이 페이지 편집