spottedcat 0.9.2

Rusty SpottedCat simple game engine
Documentation
# Image Shader Layout

This document describes the public `Image` shader contract for `spottedcat`.

The runtime does not expose `wgpu`, but custom image shaders still run against a fixed bind-group layout and a fixed vertex buffer layout.

## Modes

Generate a starter WGSL with:

```rust
let wgsl = spottedcat::image_shader_template();
```

If you want the generated template to include the extra-texture bind group, use the template API:

```rust
let wgsl = spottedcat::ImageShaderTemplate::new()
    .with_extra_textures(true)
    .build();
```

For the highest control, register the edited shader with:

1. `register_image_shader_desc(ctx, ImageShaderDesc::from_wgsl(source))`
   - Full WGSL mode.
   - You provide the full shader source.
   - This is the only supported custom `Image` shader path.

The recommended path is the limited template API:

```rust
let shader_id = spottedcat::register_image_shader_template(
    ctx,
    spottedcat::ImageShaderTemplate::new()
    .with_extra_textures(true)
    .with_shared("fn tint(c: vec3<f32>) -> vec3<f32> { return c * vec3<f32>(1.0, 0.5, 0.8); }")
    .with_vertex_body("out.local_uv = out.local_uv * 0.9 + vec2<f32>(0.05, 0.05);")
    .with_fragment_body("return vec4<f32>(tint(src.rgb), src.a * opacity);")
);
```

Supported slots:

1. `shared`
   - helper functions, constants, and shared WGSL declarations
2. `vertex_body`
   - a small vertex-stage customization block inserted before `return out`
3. `fragment_body`
   - the body inserted into `fs_main` after `src` and `opacity` are prepared

## Pipeline Contract

These parts stay engine-defined even in full WGSL mode:

1. Render pipeline topology: triangle strip quad rendering.
2. Draw model: one quad per image draw.
3. Vertex buffer layout: the engine writes one instance record per draw.
4. Uniform payload for custom data: `ShaderOpts` as `array<vec4<f32>, 16>`.
5. Extra texture count limit: up to 4.

The blend mode is configurable through `ImageShaderBlendMode`.

## Bind Group Layout

### Without extra textures

1. `@group(0)` source image texture and sampler
2. `@group(1)` user globals
3. `@group(2)` engine globals

### With extra textures

If `ImageShaderDesc::with_extra_textures(true)` is used:

1. `@group(0)` source image texture and sampler
2. `@group(1)` extra textures and sampler
3. `@group(2)` user globals
4. `@group(3)` engine globals

The extra-texture bind group always exposes 4 texture bindings plus 1 sampler. If your shader only needs 1 or 2 extra textures, you can ignore the unused bindings.

## Binding Details

### `@group(0)`: source image

```wgsl
@group(0) @binding(0) var tex: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;
```

This is the image passed as the `source` argument to `draw_with_shader` or `draw_with_shader_bindings`.

### `@group(1)`: extra textures

Present only when `with_extra_textures(true)` is enabled.

```wgsl
@group(1) @binding(0) var t0: texture_2d<f32>;
@group(1) @binding(1) var t1: texture_2d<f32>;
@group(1) @binding(2) var t2: texture_2d<f32>;
@group(1) @binding(3) var t3: texture_2d<f32>;
@group(1) @binding(4) var extra_samp: sampler;
```

These slots are populated from `ImageShaderBindings`.

```rust
let bindings = ImageShaderBindings::new()
    .with_extra_image(0, noise)
    .with_screen(1)
    .with_history(2);
```

Slot mapping:

1. `with_extra_image(slot, image)` -> samples another `Image`
2. `with_screen(slot)` -> samples the current target snapshot
3. `with_history(slot)` -> samples the previous-frame target snapshot

Unused slots fall back to the source image texture.

## `screen` And `history`

`screen` and `history` are target-relative, not global.

For a draw into some target texture `T`:

1. `screen`
   - A snapshot of `T` taken before the target's image batch begins.
   - For the main screen, this means the state after the 3D/base pass and before the 2D image batch.

2. `history`
   - A snapshot of `T` from the end of the previous frame.
   - On the first frame, it falls back to `screen`.

Important: `screen` is not a live read of the current attachment during the same pass.

## User Globals Layout

Custom per-draw data comes from `ShaderOpts`.

WGSL declaration:

```wgsl
@group(N) @binding(0) var<uniform> user_globals: array<vec4<f32>, 16>;
```

`N` is:

1. `1` when extra textures are disabled
2. `2` when extra textures are enabled

Rust side example:

```rust
let mut opts = ShaderOpts::default();
opts.set_vec4(0, [time, 0.0, 0.0, 0.0]);
opts.set_vec4(1, [1.0, 0.5, 0.8, 1.0]);
```

## Engine Globals Layout

WGSL declaration:

```wgsl
struct EngineGlobals {
    screen: vec4<f32>,
    opacity: f32,
    shader_opacity: f32,
    scale_factor: f32,
    _padding: f32,
};

@group(N) @binding(0) var<uniform> _sp_internal: EngineGlobals;
```

`N` is:

1. `2` when extra textures are disabled
2. `3` when extra textures are enabled

Field meanings:

1. `screen.xy`
   - `2.0 / logical_width`, `2.0 / logical_height`
2. `screen.zw`
   - `1.0 / logical_width`, `1.0 / logical_height`
3. `opacity`
   - draw opacity from `DrawOption`
4. `shader_opacity`
   - `ShaderOpts::opacity`
5. `scale_factor`
   - current window/device scale factor

## Vertex Layout

The engine provides one instanced vertex record per image draw.

WGSL input shape:

```wgsl
struct VsIn {
    @builtin(vertex_index) vertex_index: u32,
    @location(0) pos: vec2<f32>,
    @location(1) rotation: f32,
    @location(2) size: vec2<f32>,
    @location(3) uv_rect: vec4<f32>,
};
```

Field meanings:

1. `pos`
   - logical draw position
2. `rotation`
   - per-draw rotation in radians
3. `size`
   - final logical draw size after scaling
4. `uv_rect`
   - source UV rectangle in atlas space

## Entry Points

In full WGSL mode, entry point names are currently fixed:

1. Vertex: `vs_main`
2. Fragment: `fs_main`

## Example

See:

1. `examples/advanced_image_shader_full.rs`

That example demonstrates:

1. full WGSL registration
2. extra image textures
3. `screen`
4. `history`
5. additive blending