MSX — MidStroke eXtension

Vector image format co-designed with DixScript and MBFA.

Why MSX?

SVG is XML. Nobody writes XML by hand for complex graphics. MSX source files are DixScript — the same format powering your project configs, but now driving your vectors. QuickFuncs become parametric shape generators and reusable component libraries. Zero repetition. Full type safety. Compile once to a compact binary that MBFA crushes further.

Workspace

MSX is a multi-crate Rust workspace, not a single library:

Crate Role
core/msx-ast Element tree, paints, defs, transforms, animation data types
core/msx-anim Keyframe timeline resolution (resolve_at_time) — TRS + opacity, every element type
core/msx-parser DixScript → Scene AST
core/msx-binary Scene ↔ compact binary .msx, optional MBFA compression
render/msx-render-core Shared Renderer/RenderTarget traits
render/msx-render-svg Scene → SVG export
render/msx-render-cpu Scene → raster PNG, pure CPU (tiny-skia)
render/msx-render-gpu Scene → raster PNG/GIF via wgpu — the only backend that actually executes Def::Shader WGSL; optional (gpu Cargo feature)
primitives/msx-sdf Signed-distance-field shape tree (Sdf elements)
primitives/msx-splat Gaussian-splat elements
apps/msx-cli The msx command-line tool
apps/msx-viewer Native window viewer (CPU renderer only — see Known Gaps)

Quick Start

# Requires ../mbfa as a sibling directory
cargo build --release

# Compile MSX source to binary
cargo run --release -- compile examples/basic_shapes.msx -o out.msx

# Evaluate MSX source (or decode binary) to SVG
cargo run --release -- render examples/basic_shapes.msx -o out.svg
cargo run --release -- render out.msx -o recovered.svg

# Rasterize to PNG (CPU — always available)
cargo run --release -- rasterize examples/basic_shapes.msx -o out.png

# Sample a keyframed scene's timeline to a looping GIF (CPU)
cargo run --release -- animate examples/sdf_splat_orbit.msx -o out.gif

# Roundtrip self-test (source → binary → SVG → compare)
cargo run --release -- roundtrip examples/basic_shapes.msx

# Parse + schema-validate only, no output
cargo run --release -- validate examples/basic_shapes.msx

# Show canvas/element/def stats for a file (source or binary)
cargo run --release -- info out.msx

# --- GPU rendering (real WGSL shader execution) needs the `gpu` feature ---
cargo build --release --features gpu

# Rasterize with real Def::Shader WGSL execution, single frame at t = <seconds>
cargo run --release --features gpu -- rasterize-gpu examples/shader_orb.msx -o out.png --time 1.5

# Sample BOTH the msx-anim keyframe clock and the shader's time uniform
# together into a GIF — see "Animation" below for why there are two clocks
cargo run --release --features gpu -- animate-gpu examples/shader_orb.msx -o out.gif --duration 4 --fps 24

# Run all tests
cargo test -- --nocapture

# Run benchmarks
cargo bench --bench compare

MSX Source Format

An .msx file is a valid DixScript file with a vector-graphics schema.

@CONFIG(
  version -> "1.0.0"
)

@ENUMS(
  FillRule { NonZero = 0, EvenOdd = 1 }
)

@QUICKFUNCS(
  // Reusable style shorthand
  ~s<object>(fill, stroke, sw) {
    return { fill = fill, stroke = stroke, stroke_width = sw, opacity = 1.0 }
  }

  // Parametric badge component — DixScript QuickFuncs compose freely
  ~badge<object>(x, y, label, color) {
    return {
      type     = "group"
      elements = [
        { type = "rect", x = x, y = y, width = 90, height = 30, rx = 15,
          style = { fill = color, stroke = "none", stroke_width = 0, opacity = 1.0 } }
        { type = "text", x = x + 45, y = y + 20, content = label,
          style = { fill = #fff, font_size = 12, text_anchor = "middle",
                    stroke = "none", stroke_width = 0, opacity = 1.0 } }
      ]
    }
  }
)

@DATA(
  // Canvas definition
  scene: { width = 600, height = 200, background = #f0f0f0 }

  // Every element is a plain DixScript object or a QuickFunc call
  elements::
    badge(40,  80, "primary", #007bff)
    badge(160, 80, "success", #28a745)
    badge(280, 80, "danger",  #dc3545)
    { type = "circle", cx = 450, cy = 100, r = 60,
      style = s(#533483, #7d3c98, 3) }
)

Element types

rect, circle, ellipse, line, polyline, polygon, path, text, group, use, layer (isolated-buffer compositing — see "Layers" below), sdf (signed-distance-field shapes, primitives/msx-sdf), splat (Gaussian-splat elements, primitives/msx-splat), and image (file-ref or embedded base64 PNG/JPEG/GIF, format-sniffed at parse time).

Def types

Referenced by any element's fill/stroke via "url(#id)": linear_gradient, radial_gradient, conic_gradient, shader — a real WGSL fragment shader, executed for real on msx-render-gpu — and audio (file-ref or embedded base64 WAV/OGG/MP3, referenced by id rather than painted — see "Animation" and msx extract-media for the only way to currently get audio bytes back out and verify them).

defs::
  { type = "shader", id = "my_shader", source_ref = "shaders/my_shader.wgsl",
    entry_point = "fs_main", fallback_color = #7c5cff,
    uniforms = [ { name = "speed", type = "float", value = 1.0 },
                 { name = "resolution", type = "vec2", value = [300.0, 200.0] } ] }
elements::
  { type = "rect", x = 0, y = 0, width = 300, height = 200,
    style = { fill = "url(#my_shader)", stroke = "none", stroke_width = 0, opacity = 1.0 } }

source_ref is a WGSL fragment-shader-only file, resolved relative to the .msx file's own directory. Its uniform struct must declare the uniforms list above in the same order, followed by an auto-appended trailing time: f32 the renderer drives every frame — see render/msx-render-gpu/src/shader.rs's module doc for the exact byte-layout contract (WGSL's real vec2/vec3/vec4 alignment rules, not a simplified approximation). fallback_color is what every renderer except msx-render-gpu paints instead — see "Rendering Backends" below.

Every .msx file under examples/ that uses a shader is a working reference: shader_placeholder.msx (a simple animated plasma effect), shader_checkerboard.msx (a deliberately trivial diagnostic pattern — useful when debugging the pipeline itself, not decoration), and shader_orb.msx (a raymarched, rim-lit sphere — the heaviest WGSL of the three, loop/branch-heavy rather than flat per-pixel math).

Rendering Backends

Three renderers, not one — they don't all support the same things:

Backend Crate Def::Shader fills Notes
CPU raster msx-render-cpu Flat fallback_color only Always available, no feature flag
SVG export msx-render-svg Flat fallback_color only msx render
GPU raster msx-render-gpu Real WGSL execution Behind --features gpu; needs a GPU adapter (real or software/lavapipe)

Within msx-render-gpu specifically, real shader execution doesn't reach everywhere yet — the WGSL only runs where the geometry is either a triangulated shape or has an explicit compositing path built for it:

Element type Flat color / gradient fill Real shader fill
Vector shapes (rect, circle, path, …) ✅ fill only, not inside a layer
sdf ✅ fill only, not inside a layer — composited against the node's real antialiased silhouette (mask × shader-color), not just its bounding box
splat ✅ (flat color field, no Paint/def reference at all) ❌ — splats have no fill-reference concept yet, structurally
Anything inside a layer ❌ — layer.rs doesn't route to the shader pipeline yet, for any element type

A shader fill that fails to resolve (missing source_ref, adapter unavailable, etc.) always falls back to flat fallback_color rather than vanishing or panicking — this holds for every element type above. Malformed-but-readable WGSL is the one failure mode that isn't caught gracefully yet (see shader.rs's module doc).

Animation

There are two independent clocks, and understanding which one moves what is the whole story:

1. Keyframe timeline (msx-anim, resolve_at_time) — animations:: blocks with duration/loop_mode, driving translate/scale/rotate/ opacity. Works uniformly across every element type, sdf and splat included. msx animate samples this to a GIF via msx-render-cpu. 2. Shader time uniform — the free-running clock auto-appended to every shader def's uniform struct (see "Def types" above). Only moves on msx-render-gpu; msx rasterize-gpu --time <seconds> samples one frame of it.

msx animate-gpu is the only command that drives both at the same t per frame, into one GIF — the right tool whenever a scene uses (or might use) a shader def and keyframes together. msx-viewer plays the keyframe timeline live (see apps/msx-viewer/src/playback.rs), but doesn't drive the shader time uniform — that clock is GPU-only, and live GPU playback in the viewer is a separate, not-yet-started piece of work (see Known Gaps). msx animate/msx animate-gpu remain the way to get a shareable baked GIF either way.

Layers

{ type = "layer", children = [...] } composites its children into an isolated offscreen buffer first (own opacity, own blend mode, own effects), rather than drawing straight into the shared scene buffer the way group does. Shader fills inside a layer's children DO get real GPU execution — layer.rs's own render path is given the real scene defs and the same shader pipeline/composite the top-level path uses, not a stripped-down one. Non-Normal blend modes (multiply, screen, etc.) and the blur effect are implemented on all three renderers; drop_shadow/inner_shadow/outer_glow/inner_glow are CPU/SVG only so far — see msx-render-gpu/src/effects.rs's own module doc for the shared foundation those four would build on.

Binary Format (MSX)

Header (32 bytes):

[0..4]   magic:      0x4D 0x53 0x58 0x00  ("MSX\0")
[4]      version:    u8 = 2
[5]      compress:   u8  (0=none  1=mbfa)
[6]      flags:      u8  (bit0=has_viewbox  bit1=has_metadata  bit2=has_defs  bit3=has_animations)
[7]      reserved:   u8
[8..12]  width:      f32 LE
[12..16] height:     f32 LE
[16..20] elem_count: u32 LE
[20..24] str_pool_len: u32 LE
[24..28] def_count:  u32 LE
[28..32] reserved:   [u8; 4]

Payload (optionally MBFA-compressed):

Background RGBA    4 bytes
Viewbox            16 bytes  (if flags bit 0)
String pool        [u16 count][u16 len + bytes]*
Def section        [element]* — gradient, pattern, shader, AND audio defs
Element stream      [element]*  (recursive, terminated by 0xFF) — every
                    element type above, including sdf/splat/layer/image
Animation section   duration/loop_mode/animations, only if flags bit 3

Full tag values and per-type wire layouts are in docs/format-spec.md, updated alongside this README rather than left to drift — it now covers sdf/splat/shader/image/audio defs and elements, layer (including blend modes and effects), the animation section below, and the CLI's actual current command list.

The binary codec now serializes animations/duration/loop_mode — compiling an animated scene to binary no longer silently drops its keyframe tracks (previously an open, confirmed gap; closed with real round-trip tests in core/msx-binary/src/compiler.rs).

MBFA Co-design

Vector binary data has structure MBFA exploits:

  • Coordinate streams — adjacent elements often share x/y proximity → LZ back-references span across shapes
  • Opcode streams — repeated path command sequences (M L L Z over and over) → fold-1 matches, fold-2 pair-encodes repeated opcodes
  • Color data — palettes tend to repeat; RGB bytes of similar colours are nearby values → delta-encoding before MBFA degrades entropy fast

License

MIT