Expand description
HLSL to WGSL Translation
Pragmatic, MilkDrop-2-targeted translator. The MD2 user shader body lives
inside a shader_body { ... } block, samples the previous frame via
tex2D / GetPixel / GetBlur1..3, and uses HLSL-style typed local
declarations (float2 uv2;). The translator turns that into a WGSL
fragment-body fragment that the codegen wrapper can paste inside its
fs_main.
The rewrites are still string-driven (no AST) โ but they understand the MD2 conventions enough to land the dominant cases.
Re-exportsยง
pub use types::SymbolTable;pub use types::WgslType;
Modulesยง
- ast
- HLSL AST.
- lex
- HLSL tokenizer.
- parse
- HLSL recursive-descent parser.
- rewrite
- AST-driven HLSLโHLSL rewrites.
- texture_
plan ๐ - Per-preset user-texture binding plan + the
tex2Drewriter that consumes it. - types
- Type-aware post-translator passes for the HLSLโWGSL pipeline.
Structsยง
- Texture
Binding Plan - Per-preset mapping from HLSL sampler names to the comp passโs user-texture slots.
- Texture
Slot - One slot in a
TextureBindingPlan. Carries the resolved pool texture name (orNonewhen the renderer should fall back to the 1ร1 white fallback) and the textureโsvec4<f32>(w, h, 1/w, 1/h)for thetexsize_<NAME>constant the wrapper emits. - User
Sampler Ref - One parsed
sampler sampler_X;declaration. The renderer consumes this to populate aTextureBindingPlan.
Enumsยง
Constantsยง
- KNOWN_
CALL_ ๐NAMES - HLSL function names that the rewrite pipeline treats specially. When a
preset writes
name (args)with whitespace between the identifier and(, downstream substring matches ("lerp(", paren-balanced walkers) would skip the call. Collapse that whitespace once up-front so every downstream pass sees the no-space form. - LIFTED_
FN_ SENTINEL - Marker line that separates module-scope user functions (lifted by
lift_user_functions) from the fragment body inside the translated output. The codegen wrapper splits on this marker: text before goes beforefs_main, text after goes inside it. - MAX_
USER_ TEXTURE_ SLOTS - Maximum simultaneously-bound user textures per preset. Sized to cover
the in-the-wild distribution: a typical preset survey shows โค 4
distinct
sampler sampler_X;declarations per shader; 8 leaves headroom without blowing past the comp pipelineโs bind-group budget. Presets that exceed this cap are translated with the first 8 slots used and the rest falling through to the standard fallback path (sampler_main).
Staticsยง
- CONST_
TYPE_ ๐REGEX - HLSL
const TYPE NAMEat start of a typed local declaration. We stripconstonly when followed by a recognised HLSL type token โ leaves WGSL module-levelconst(which doesnโt appear in user shader bodies anyway) alone. - INLINE_
DECL_ ๐SPLIT_ REGEX - Normalise
;<space>TYPEto;\nTYPEso the line-anchoredLOCAL_DECL_REGEXsees each declaration on its own line. The Isosceles preset (and a handful of MD2 packs that compress kaleidoscope state) put multiple typed locals on a single source line: - LEADING_
ZERO_ ๐REGEX - Strips leading zeros from integer literals (HLSL allows
02for2, WGSL rejects them withinvalid numeric literal format). Targets only integer literals โ0.5and0and100are untouched because the pattern requires\b0+followed by another decimal digit (so0.and0)never match). - LOCAL_
DECL_ ๐REGEX - HLSL typed local declaration, post type-substitution. The whole
<TYPE> <decls>;statement is captured; the declarator list is then expanded into one WGSLvarper name. Examples:f32 gx1 = a;โvar gx1: f32 = a;vec2<f32> uv2;โvar uv2: vec2<f32>;vec3<f32> ret1, neu, crisp;โvar ret1: vec3<f32>; var neu: vec3<f32>; var crisp: vec3<f32>; - POSTFIX_
DEC_ ๐REGEX - POSTFIX_
INC_ ๐REGEX - Postfix
<ident>++and<ident>--(HLSL increment/decrement). WGSL has no postfix operators; we rewrite to the equivalent compound assignment<ident> = <ident> + 1only at statement boundaries (;or)) so expression-position uses likea[i++]donโt get mangled. Real preset pattern:n++;at end of a per-iteration loop. - PREPROC_
REGEX ๐ - Preprocessor directives (
#define,#include,#pragma, โฆ). HLSL presets occasionally use them; WGSL has no preprocessor, so we strip the whole line. - SAMPLER_
DECL_ ๐REGEX - HLSL
sampler foo;/texture foo;declarations at module scope. The codegen wrapper provides the actual texture/sampler bindings, so user declarations are redundant โ and they confuse the WGSL parser when they land insidefs_main. Stripped out wholesale. - SEMANTICS_
REGEX ๐ - SHADER_
BODY_ ๐OPEN shader_bodykeyword optionally followed by whitespace/newlines and{. We then balance braces ourselves to recover the body โ a regex alone canโt do nested-brace balancing reliably.- STORAGE_
CLASS_ ๐REGEX - HLSL storage-class qualifiers โ strip when they appear as a leading
word in a declaration.
static constis a common HLSL pattern for โfunction-scope compile-time constantโ, but WGSL inside a function useslet(orconstat module scope only). Stripping both letsrewrite_local_declarationsturn the rest into a regularvar.
Functionsยง
- body_
assigns_ ๐to - Whether
body(a{ ... }WGSL block as text) contains an assignment to a bare identifier matchingname. Recognises plain=, compound assignments (+=,-=,*=,/=,%=) and post-increment / decrement (++/--); excludes the comparison operators==,!=,<=,>=. Skips matches inside//-style line comments because the textual scan would otherwise see a commented-out assignment as a real write. - brace_
up_ ๐single_ statement_ blocks - Wrap single-statement
if/while/forbodies in{ ... }. WGSL requires braces on every conditional/loop body; HLSL doesnโt, and a lot of MD2 preset code uses the brace-less form (if (cond) ret.z -= 0.5;). - collect_
global_ ๐var_ types - Re-parse
hlsl(the original input, before any rewriter touched it) and return a map ofname โ wgsl_typefor top-levelItem::GlobalVaritems. Used byhoist_global_varsto decide which body-levelvardeclarations belong at module scope. - comment_
out_ ๐prose_ lines - Comment out lines that look like prose (English) rather than
HLSL/WGSL code. Real preset pattern:
comp_30=`written by martinโ an attribution typed without a//prefix, threaded into the shader body by the.milkparser as a literal line of code. Many presets failed withexpected assignment or increment/decrement; found 'by'(orfound 'rota', etc.) on lines of this shape. - dedup_
var_ ๐declarations - Walk the source for
var <NAME>: <TYPE> [= INIT];declarations; the first time aNAMEappears in the current scope, keep it; every later declaration of the sameNAMEin the same scope becomes a plain assignment (NAME = INIT;) or โ if it had no initialiser โ is dropped entirely. - expand_
simple_ ๐defines - Scan for lines of the form
#define IDENT IDENT(whitespace-separated single-token replacement) and substitutefrom โ toeverywhere else in the source. Operates as a single pass: defines are collected first, then applied to the rest of the source. Skips macros whosetolooks like anything other than a bare identifier so we donโt accidentally inline#define K 0.5(where the rest of the source has plainKin arithmetic context โ the existing fall-through preserves it as an undefined-but-untouched identifier the user can spot in the error). - hlsl_
type_ ๐to_ wgsl - Map an HLSL type name (
float,float2,int3,float3x3, โฆ) to its WGSL equivalent (f32,vec2<f32>,vec3<f32>,mat3x3<f32>, โฆ). ReturnsNonefor types the hoist pass shouldnโt touch (e.g. user struct names, samplers โ these donโt show up asGlobalVaranyway, but be defensive). - hoist_
global_ ๐vars - Hoist top-level user globals to module scope.
- is_
function_ ๐signature - Detect โthis
declscapture is actually a function signatureโ.declsis the text the local-decl regex captured between the type and the terminating;. A function signature shape is<ident>(...) { ... return ...with the;being the first statement-terminator inside the body, but the giveaway sits at the front: a(appears before any=. Variable declarations never put(ahead of the initializer assignment (var x = sin(0);โ(follows=). - is_
unary_ ๐context - is_
wgsl_ ๐builtin_ function_ name truewhennamecollides with a WGSL builtin function. List covers the subset MD2 user shaders actually invoke; anything outside this set isnโt worth worrying about (real authors donโt shadowinverseSqrt).- keyword_
at ๐ - Match
if/while/forkeywords on word boundaries; return their length. - lift_
user_ ๐functions - Find HLSL-shaped function definitions (
<TYPE> <name>(...) { ... }) at depth 0 in the translated body, rewrite each signature to WGSL shape (fn name(arg: TYPE, โฆ) -> TYPE { ... }), and remove them fromsrcin place. The lifted functions are returned as a single string concatenated in source order โwrap_user_comp_shader_with_planplaces it beforefs_main. - looks_
like_ ๐prose - normalise_
call_ ๐whitespace - Replace
name<WS>(withname(for every entry inKNOWN_CALL_NAMES. Only fires whennameis on a word boundary and<WS>is non-empty whitespace (so the no-whitespace form is left alone). Skips matches inside/* */and//comments so commented-out code stays stable. - parse_
hlsl_ ๐params - Parse an HLSL parameter list into (type, name) pairs in source order.
Same shape as [
convert_hlsl_params_to_wgsl] but returns the structured form so callers can decide per-param whether to rename / shadow. - parse_
wgsl_ ๐function_ return_ type - Match one of the known WGSL types at byte position
i. Returns the canonical type text and the byte position immediately after it.voidis rejected โ user comp shader functions always return a typed value in MD2. - rename_
reserved_ ๐identifiers - Rename WGSL-reserved keywords used by MD2 preset authors as locals
(
mod,filter,sample). Every occurrence on a word boundary that isnโt immediately followed by((a function call โ already rewritten byrewrite_mod_balancedor rejected upstream) gets a trailing_. - rename_
word_ ๐call - Rename
<from>(โ<to>(at every word boundary. Used to alias HLSL builtins that WGSL spells differently (satโsaturate,rsqrtโinverseSqrt). Differs from a plainreplace: a preset localfrsqrt = q1wonโt pick up an unwantedfrinverseSqrt = q1rewrite because we require a non-identifier byte (or start of source) to the left of the match. - replace_
functions ๐ - replace_
semantics ๐ - replace_
statement_ ๐commas - HLSL allows comma-as-statement-separator at the top of a function body:
- replace_
types ๐ - rewrite_
binary_ ๐call_ balanced - Generic paren-balanced rewriter for two-argument calls. Walks the
source, matches
<name>(on a word boundary, finds the top-level,, and replaces the whole call with the closureโs output. - rewrite_
local_ ๐declarations - rewrite_
mod_ ๐balanced mod(a, b)โ((a) - floor((a) / (b)) * (b)). WGSL reservesmodas a keyword; HLSL uses it as the float-modulo helper. The expansion matches HLSLโs semantics (and matches GLSLโsmod) so behaviour stays identical.- rewrite_
mul_ ๐balanced mul(a, b)โ(a) * (b). Paren-balanced on both arguments โ needed because real shaders writemul(rotation_matrix(theta), uv). The outermost,at depth 0 splits the two arguments.- rewrite_
postfix_ ๐inc_ dec - rewrite_
tex2dbias ๐ tex2Dbias(s, vec4(uv, mip, bias))โtex2D(s, uv). Paren-balanced over both arguments. The bias component is dropped (real presets use it cosmetically at 0 or near-0 โ no visual delta).- rewrite_
tex3d_ ๐calls - Rewrite
tex3D(<sampler>, <uvw>)to a real 3DtextureSampleagainst the noise-volume bindings. - rewrite_
unary_ ๐call_ balanced - Generic paren-balanced rewriter for
<name>(<single-arg>)calls. Walks the source, finds<name>on a word boundary followed by(, balances to the matching), and replaces the whole call withmake_replacementapplied to the captured argument text (verbatim, not trimmed). - scan_
user_ samplers - Extract every
sampler sampler_X;declaration from a MilkDrop comp shader HLSL and return the logical name (the part aftersampler_) for each occurrence that isnโt already a built-in. - split_
param ๐ - Split a single HLSL parameter declaration into
(type, name). The type may contain<...>(vec3<f32>); we split on the last whitespace at angle-depth 0. - split_
top_ ๐level_ commas - Split a declarator list on top-level commas only โ commas inside
()or<>(e.g.vec3<f32>(0, 0, 0)) must not split the declarator. - strip_
first_ ๐vec_ component - For a string like
vec4(uv, 0, 0.1)orfloat3(uv, 0), returnuvโ the slice up to the first top-level comma inside the constructor. Used byrewrite_tex2dbiasto drop the mip-bias arguments. - strip_
preprocessor ๐ - strip_
sampler_ ๐declarations - strip_
shader_ ๐body_ wrapper - MD2 ships warp/comp shaders wrapped in a
shader_body { ... }block. The codegen wrapper pastes the user code inside its ownfs_main { ... }, so the outer wrapper has to come off first โ otherwise WGSL sees a stray identifier (shader_body) followed by{and fails withexpected assignment or increment/decrement, found "{". - strip_
storage_ ๐class_ qualifiers - strip_
unary_ ๐plus - Strip HLSL unary
+(a syntactic no-op WGSL doesnโt accept) when it directly follows(,,,=,+,-,*,/,<,>,?,:after optional whitespace. Preserves byte positions of everything except the+itself. - translate_
shader - Translate HLSL shader code to WGSL.
- translate_
shader_ with_ plan - Same as
translate_shader, but routes unrecognisedtex2Dsampler names through the suppliedTextureBindingPlan. Preset authors reference disk-loaded textures viasampler sampler_<NAME>;+tex2D(sampler_<NAME>, uv); the renderer scans the HLSL, builds a plan that resolves each name to a slot in the comp pipelineโs user-texture binding array, and threads it through here so the emitted WGSL points at the right binding. - try_
extract_ ๐user_ function - Try to match a single HLSL-shaped function definition starting at byte
position
start(after leading whitespace). Returns the byte position just past the closing}and the rewritten WGSL function text. ReturnsNoneif no signature matches โ caller advances by 1 byte. - user_
texture_ binding_ name - WGSL binding name for user-texture slot
slot. The codegen wrapper declaresvar sampler_user_<n>_texture: texture_2d<f32>for each slot0..MAX_USER_TEXTURE_SLOTS, and this is what the translator emits intextureSample(...)calls for plan-routed samplers.