Skip to content

(define …)

Defines a compile-time numeric constant collected into the emitter’s expression environment, referenced throughout the document.

(define :name NAME :value 2048)
Key Type Required Description
:name symbol Yes Constant name
:value number Yes A real (negative and fractional allowed), an expression over other constants, or another constant’s name

The value is usually a literal. An expression works too, and so does another constant’s name, declared before or after: constants resolve document-wide, and the one shape that cannot resolve is a cycle, refused on the first constant in it. Nothing about the define says “integer”: the slot that reads it decides, so (define :name HALF :value 0.5) is fine in :clear-value and a located refusal in :vertex-count.

(define :name NUM_PARTICLES :value 2048)
(define :name SAMPLE_COUNT :value 4)
(define :name WORKGROUP_SIZE :value 64)

A defined constant is a value in every numeric slot: on its own, or inside a compile-time expression.

(define :name NUM_PARTICLES :value 4096)
(define :name PARTICLE_SIZE :value 16)
(define :name WORKGROUP_SIZE :value 64)
(buffer :name particles
:size (* NUM_PARTICLES PARTICLE_SIZE)
:usage [storage])
(compute-pass :name simulate
:pipeline simPipeline
(dispatch :workgroups [(ceil (/ NUM_PARTICLES WORKGROUP_SIZE))]))

The expression (* NUM_PARTICLES PARTICLE_SIZE) evaluates at compile time over the (define …) constants. See Expressions for the full function and operator set.

Every numeric slot takes one: a byte size or offset, a binding or group index, a pool size, a device limit, a sampler’s :max-anisotropy, a vector element, as well as the draw and dispatch counts.

(define :name SIZE :value 32768)
(define :name ANISO :value 4)
(define :name WG :value 64)
(buffer :name particles :size SIZE :usage [storage])
(sampler :name smooth :max-anisotropy ANISO
:mag-filter linear :min-filter linear :mipmap-filter linear)
(compute-pass :name step :pipeline simPipeline
(dispatch :workgroups [WG 1 1]))

The name is checked against the document’s (define …) forms, so a misspelling is a not_cross_ref diagnostic on that slot rather than a silent default, and a literal that misses the slot’s own bounds still reports those bounds.

Two slots are the exception, for the same reason: their symbol spelling is already taken by a member set, so a bare name is not recognised as a constant and the constant is written as arithmetic instead. :write-mask (member all) reports a bare name as not_member; write :write-mask (* MASK 1). A (wasm-call :args […]) element (members canvas-width, canvas-height, time-total, time-delta) reports it as union_no_branch_matched; write (* SCALE 1).

One constant keeps the render target and the pipeline in step: the same name goes in the texture’s :sample-count and in the pipeline’s (multisample :count …).

(define :name SAMPLE_COUNT :value 4)
(texture :name msaaTarget :size canvas
:format preferred-canvas-format
:usage [render-attachment]
:sample-count SAMPLE_COUNT)
(define :name NUM_PARTICLES :value 8192)
(define :name PARTICLE_STRIDE :value 32)
(define :name WORKGROUP_SIZE :value 256)
(buffer :name particleBuffer
:size (* NUM_PARTICLES PARTICLE_STRIDE)
:usage [storage vertex])
(compute-pass :name update
:pipeline simPipeline
(dispatch :workgroups [(ceil (/ NUM_PARTICLES WORKGROUP_SIZE))]))
(render-pass :name render
(color-attachment :view context-current-texture
:clear-value [0 0 0 1] :load-op clear :store-op store)
:pipeline renderPipeline
(draw :vertex-count 6 :instance-count NUM_PARTICLES))
(define :name VERTEX_COUNT :value 1000)
(define :name FLOATS_PER_VERTEX :value 8)
(define :name BYTES_PER_FLOAT :value 4)
(buffer :name vertices
:size (* VERTEX_COUNT FLOATS_PER_VERTEX BYTES_PER_FLOAT)
:usage [vertex])

Defines are document-wide and can be referenced:

  • Inside any compile-time expression, (* NAME 16) or (ceil (/ NAME 64))
  • As a bare name in any numeric slot, :size NAME
  • From another (define …), before or after it, whose :value may be an expression over the rest

Order never matters: a (define …) may sit below the form or the constant that uses it, because the whole document’s constants are resolved before lowering and emission run. A cycle is the one refusal.

Rule Error
Two (define …) forms may not share a name duplicate_cross_ref_target
A name already taken by another form is a clash: names are one namespace duplicate name 'N': already declared as a (buffer …) at line L
A constant that depends on itself, directly or through another (define :name A) depends on itself (located)
A fractional or negative constant in an integer slot :vertex-count evaluates to 1.5, and this slot takes an integer (located on the slot)
A referenced constant must be declared not_cross_ref
:write-mask and a (wasm-call :args …) element take arithmetic, not a bare name not_member / union_no_branch_matched

Unlike WGSL const, a (define …) is resolved at PNGine compile time (not shader compile time). Use it for values that affect resource creation.

; PNGine compile time
(define :name SIZE :value 256)
(buffer :name buf :size SIZE :usage [uniform]) ; 256-byte buffer
; For shader constants, use WGSL const inside the module:
(shader-module :name code :code """
const SIZE: u32 = 256; // WGSL compile time
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3u) {
if (id.x >= SIZE) { return; }
}
""")