# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a cross-platform Shadertoy-like fragment shader viewer built with SDL3. It supports two rendering backends:
- **SDL3 GPU** (default): renders via Vulkan on Linux/Windows and Metal on macOS.
- **OpenGL 3.3** (fallback): the original backend, used automatically if SDL3 GPU init fails.

Selection at launch:
```bash
./shadertoy --backend=gpu     # force SDL3 GPU
./shadertoy --backend=opengl  # force OpenGL
./shadertoy --backend=auto    # default: GPU first, fall back to OpenGL on failure
```

## Build and Development Commands

### Building (CMake - Development)
```bash
mkdir build && cd build
cmake ..
cmake --build . --config Release
```

### Building (Makefile - Release Packages)
```bash
make windows_release   # Creates .zip with DLLs
make macos_release     # Creates .dmg with app bundle
make linux_release     # Creates .tar.gz
make show_version      # Display build version (YYYY-MM-DD format)
```

Platform-specific debug builds:
```bash
make windows_debug
make macos_debug
make linux_debug
```

### Compiling Shaders to SPIR-V
The SDL3 GPU backend reads compiled SPIR-V (`.frag.spv`) at runtime. Run this after touching any `.vk.glsl` file:
```bash
cmake --build build --target compile_shaders
```
Requires `glslc` (Debian/Ubuntu: `apt install glslang-tools`; macOS: `brew install glslang`).

### Running the Application
```bash
./shadertoy [SHADER_NAME_OR_PATH] [-F|--fullscreen] [--backend=auto|gpu|opengl]

# Examples:
./shadertoy test                            # auto backend, data/shaders/test/
./shadertoy --backend=gpu seascape          # force SDL3 GPU (Vulkan/Metal)
./shadertoy -F data/shaders/fractal_pyramid # explicit folder path, fullscreen
```

**Runtime Controls:**
- `ESC` - Exit
- `F3` - Toggle fullscreen
- `LEFT/RIGHT ARROW` - Cycle through shaders in directory

## Architecture

### Layered design
- `source/main.cpp` — SDL3 window, event loop, shader-list scanning, dispatch to a backend.
- `source/rendering/shader_backend.hpp` — abstract `IShaderBackend` interface + `ShaderMetadata` / `ShaderUniforms` / `ShaderProgramSpec` shared types. Two factories: `makeOpenGLBackend()`, `makeSdl3GpuBackend()`.
- `source/rendering/opengl_shader_backend.{hpp,cpp}` — OpenGL 3.3 implementation. Loads `<folder>/<name>.gl.glsl` at runtime.
- `source/rendering/sdl3gpu/sdl3gpu_shader_backend.{hpp,cpp}` — SDL3 GPU implementation. Loads `<folder>/<name>.frag.spv` (Linux/Windows) or `<folder>/<name>.frag.msl` (macOS); shares one passthrough vertex shader from `shaders/_common/passthrough.vert.{spv,msl}`.
- `source/rendering/sdl3gpu/shader_factory.hpp` — small helper that loads a shader binary/source from disk and creates an `SDL_GPUShader`.

### Shader folder layout
One folder per shader, under `data/shaders/`:
```
data/shaders/<name>/
    <name>.gl.glsl    # OpenGL GLSL 3.30 (handwritten)
    <name>.vk.glsl    # Vulkan GLSL 4.50 (handwritten)
    <name>.frag.msl   # Metal Shading Language (handwritten)
    <name>.frag.spv   # generated by `make compile_shaders` from .vk.glsl
    meta.txt          # Name / Author / iChannelN
data/shaders/_common/
    passthrough.vk.glsl    # shared fullscreen-triangle vertex (Vulkan)
    passthrough.vert.spv   # generated
    passthrough.vert.msl   # handwritten Metal
```
Folders starting with `_` or `.` are skipped by the scanner. The `meta.txt` parser is in `Rendering::parseMetaFile`.

### Backend selection logic (`main.cpp`)
1. Parse `--backend=...` flag (default `auto`).
2. If not OpenGL: create a window with no flags, instantiate `Sdl3GpuShaderBackend`. If `init()` fails AND choice was `auto`, destroy the window and continue. If choice was `gpu`, exit with error.
3. Otherwise: create a window with `SDL_WINDOW_OPENGL` and instantiate `OpenGLShaderBackend`.

### Global State (main.cpp)
```cpp
shader_list_           // Vector<ShaderEntry { folder, base_name }>
current_shader_index_  // Active shader in rotation
shader_start_ticks_    // Base time for iTime uniform calculation
window_                // SDL3 window pointer
backend_               // unique_ptr<IShaderBackend>
shaders_directory_     // Shaders root path (resolved at startup)
```

### Dependencies
- **SDL3** ≥ 3.2 — windowing, events, GPU API (`SDL_gpu.h`)
- **GLAD** — OpenGL 3.3 loader (used only by the OpenGL backend, statically linked via `source/external/glad/`)
- **C++17 stdlib** — filesystem, fstream, vector, algorithms
- Build-time tool: `glslc` (only required to regenerate `.frag.spv`)

### Platform-Specific Code
Uses preprocessor defines (`WINDOWS_BUILD`, `MACOS_BUILD`, `LINUX_BUILD`) for:
- `getExecutableDirectory()` - Windows API, mach-o dyld, or /proc/self/exe
- `getResourcesDirectory()` - Special macOS app bundle handling (Contents/Resources)
- Linking flags - Windows uses static linking, macOS links OpenGL framework

## Shader System

### Authoring a shader (manual steps)
For each new shader, three handwritten files live in `data/shaders/<name>/`:
1. **`<name>.gl.glsl`** — OpenGL GLSL 3.30 (the original Shadertoy-style format described below).
2. **`<name>.vk.glsl`** — Vulkan GLSL 4.50 with explicit `layout(set=3, binding=0) uniform ShadertoyUBO { float iTime; vec2 iResolution; } u;` and `layout(set=2, binding=N) uniform sampler2D` for any texture channel.
3. **`<name>.frag.msl`** — Metal Shading Language. Entry point must be named `<name>_fs`. Uniforms come in via `[[buffer(0)]]`; samplers via `[[texture(N)]] [[sampler(N)]]`.

After editing any `.vk.glsl`, rebuild SPIR-V: `cmake --build build --target compile_shaders`.

The MSL is **not** auto-generated — write it by hand following the same convention as `aee_2026/source/core/rendering/sdl3gpu/msl/*.msl.h`.

### Shadertoy → Vulkan/Metal porting cheatsheet
- `iResolution` is `vec2` here (no `.xy`). Reconstruct vec3 manually if needed.
- Replace `texture(iChannel0, ...)` calls — no texture channels currently wired through the GPU backend (planned).
- The shared vertex stage emits `vUV` already in Shadertoy convention `(0,0)` bottom-left → `(1,1)` top-right; multiply by `iResolution` to get `fragCoord`.
- The Y axis is flipped in NDC inside the shared vertex shader so Vulkan/Metal render right-side-up like OpenGL.

### Shader Format (GLSL 3.3 Core)
All shaders must follow this structure:

```glsl
#version 330 core
precision highp float;

out vec4 FragColor;
in vec2 vUV;              // Normalized [0,1] coordinates from vertex shader
uniform vec2 iResolution; // Window resolution in pixels
uniform float iTime;      // Time since shader loaded (seconds)

// Shadertoy-style entry point
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // fragCoord is in pixel coordinates
    // Your shader code here
}

// Wrapper converts vUV to pixel coordinates
void main() {
    vec2 fragCoordPixels = vUV * iResolution;
    vec4 outColor;
    mainImage(outColor, fragCoordPixels);
    FragColor = outColor;
}
```

### Adding New Shaders
1. Create `.glsl` file in `data/shaders/<name>/` directory (use `.frag.glsl` convention)
2. Follow required format above
3. File automatically appears in runtime shader rotation (arrow keys to navigate)
4. **No code changes required** - directory is scanned at startup

### Shader Loading Pipeline
1. Directory scan on startup (`scanShaderDirectory()`)
2. Sort alphabetically
3. Load fragment shader source from disk
4. Compile vertex shader (fixed, embedded in main.cpp)
5. Compile fragment shader with error logging
6. Link program with error handling
7. Update uniforms each frame (iResolution, iTime)

### Supported Shadertoy Features
- ✅ `iTime` - Time uniform
- ✅ `iResolution` - Window resolution (vec2, not vec3)
- ✅ `mainImage()` function signature
- ❌ `iMouse` - Not implemented
- ❌ `iChannel0-3` - No texture channels (multi-pass not supported)
- ❌ `iFrame`, `iTimeDelta`, `iDate` - Not implemented

### Converting from Shadertoy
When porting Shadertoy shaders:
1. Copy the `mainImage()` function as-is
2. Add standard header (see format above)
3. Add wrapper `main()` function
4. Remove texture channel references (iChannel0-3)
5. Change `iResolution.xy` to `iResolution` (this project uses vec2)

### Common Conversion Issues and Solutions

Based on experience converting complex shaders like ddla_light_tunnel:

#### iResolution vec3 vs vec2
- **Shadertoy:** `iResolution` is `vec3(width, height, width/height)`
- **This project:** `iResolution` is `vec2(width, height)`
- **Solution:** Create vec3 manually: `vec3 r = vec3(iResolution.xy, iResolution.x/iResolution.y);`
- **Then use:** `r.xy` for resolution, `r.y` for height (as in original)

#### Uninitialized Variables
- **Problem:** Shadertoy code may have `vec3 rgb;` without initialization
- **Shadertoy behavior:** Likely initializes to `vec3(0.0)` (black)
- **This project:** Uninitialized variables contain undefined values (often causes black screen or wrong colors)
- **Solution:** Always initialize: `vec3 rgb = vec3(0.0);`
- **Wrong approach:** Don't use random noise unless shader explicitly uses iChannel texture for noise

#### Low mix() Factors Are Intentional
- **Example:** `rgb = mix(rgb, calculated_color, 0.01);` means 1% new color, 99% existing
- **Don't change these factors** - they create subtle effects intentionally
- **If output is black:** Problem is likely the base value (rgb), not the mix factor

#### iChannel Textures
- **Shadertoy shows iChannel0-3** in UI, but shader may not use them
- **If iChannel3 has RGB noise but code doesn't reference it:** The noise is not actually used
- **Check code for `texture(iChannelN, ...)` calls** - if none exist, ignore the iChannel setup
- **Procedural noise replacement:** Only if shader explicitly samples the texture

#### Color Swapping Issues
- **If colors are completely wrong:** Don't randomly swap color variables
- **First verify:** Code matches original Shadertoy exactly (except required GLSL changes)
- **Common mistake:** Changing color applications that were correct in original
- **Debug approach:** Revert to exact original code, only modify for GLSL 3.3 compatibility

#### Division by Small Values - Color Overflow Artifacts
- **Problem:** Division by values near zero causes extreme values → color overflow artifacts (green points, strange colors)
- **Example:** `.01*vec4(6,2,1,0)/length(u*sin(iTime))` can divide by ~0.0 when near center and sin≈0
- **Symptoms:** Bright white center that pulses to green, or random color artifacts in specific areas
- **Root cause:** Even with tonemap (tanh/clamp), extreme intermediate values cause precision issues or saturation
- **Solution - Double Protection:**
  1. **Add epsilon to denominator:** `max(denominator, 0.001)` prevents division by exact zero
  2. **Clamp the result:** `min(result, vec4(50.0))` prevents extreme accumulation
  3. **Only clamp problematic terms:** Don't clamp everything or scene becomes dim
- **Example fix:**
  ```glsl
  // Original (causes overflow):
  o += .01*vec4(6,2,1,0)/length(u*sin(t+t+t)) + 1./s * length(u);

  // Fixed (prevents overflow while maintaining brightness):
  vec4 brightTerm = min(.01*vec4(6,2,1,0)/max(length(u*sin(t+t+t)), 0.001), vec4(50.0));
  o += brightTerm + 1./s * length(u);
  ```
- **Key values:** epsilon ~0.001 (not too small, not too large), clamp ~50.0 (allows brightness without explosion)
- **Why this works in Shadertoy:** WebGL/browsers may handle float overflow differently than native OpenGL drivers

#### Debugging Black/Wrong Output
1. **Check compilation errors first** - shader must compile without errors
2. **Initialize all variables** - especially vec3 colors
3. **Verify vec3 iResolution handling** - create vec3 from vec2 if needed
4. **Don't modify mix factors** - keep original values
5. **Compare with original code** - ensure logic is identical
6. **Test progressive changes** - add header, test; add wrapper, test; etc.
7. **Check for division by small values** - if you see color artifacts (especially green), look for divisions that can approach zero

## Build System Details

### Version Numbering
Releases use build date format: `shadertoy-YYYY-MM-DD-{platform}` (auto-generated by Makefile)

### Release Artifacts
- **Windows:** `.zip` with `shadertoy.exe` + `SDL3.dll` + shaders
- **macOS:** `.dmg` with app bundle containing embedded SDL3.framework (arm64 only)
- **Linux:** `.tar.gz` with binary + shaders

### Resource Bundling
- Shaders copied to release packages
- LICENSE and README.md included
- Platform-specific dependencies bundled (DLLs on Windows, frameworks on macOS)

## Important Notes

### Vertex Shader
The vertex shader is hardcoded in `main.cpp` and creates a fullscreen quad. It:
- Takes `vec2 aPos` (location 0) in NDC space [-1, 1]
- Outputs `vec2 vUV` normalized to [0, 1]
- No transformation matrices needed

### Shader Hot-Reloading
Currently **not implemented**. Shader changes require application restart. The architecture would support adding this via file watching.

### Multi-Pass Rendering
Single-pass only. To add multi-pass (BufferA/B/C like Shadertoy):
- Create FBOs and textures per buffer
- Render buffers in dependency order
- Pass textures as `iChannel0-3` uniforms
- Use ping-pong for feedback loops

### OpenGL Context
Created via SDL3 with core profile (no deprecated functions). Context version: 3.3 core.