diff --git a/.vscode/settings.json b/.vscode/settings.json index e056b95c..f1ff9d29 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -106,47 +106,5 @@ "xstring": "cpp", "filesystem": "cpp", "xlocinfo": "cpp" - }, - "Lua.diagnostics.globals": [ - "bit", - "colors", - "colours", - "commands", - "disk", - "fs", - "gps", - "help", - "http", - "keys", - "multishell", - "paintutils", - "parallel", - "peripheral", - "pocket", - "rednet", - "redstone", - "rs", - "settings", - "shell", - "term", - "textutils", - "turtle", - "vector", - "window", - "_CC_DEFAULT_SETTINGS", - "_HOST", - "printError", - "write", - "read", - "sleep", - "set_pixel", - "clear", - "log", - "define_property", - "get_property", - "width", - "height", - "time", - "dt" - ] + } } \ No newline at end of file diff --git a/docs/LUA_QUICK_REFERENCE.md b/docs/LUA_QUICK_REFERENCE.md deleted file mode 100644 index 9de8e842..00000000 --- a/docs/LUA_QUICK_REFERENCE.md +++ /dev/null @@ -1,42 +0,0 @@ -# Lua Scene Development - Quick Reference - -## 1. File Setup & Execution -* **Location:** `/lua_scenes/` -* **Loading:** Directory is scanned only on startup (new files require a server restart). -* **Hot-Reload:** Saving an *existing* running script automatically rebuilds the state and updates logic (note: property structures won't update without a restart). - -## 2. Script Contract -* **`name`** (Global String): **Required**. Must be unique, no spaces (e.g., `name = "my_scene"`). -* **`external_render_only` (Global bool): **Optional**. If true, marks this scene as heavy computation and will use an external render source instead of the RPI -* **`parallel_deterministic` (Global bool): **Optional**. If true, the scene will be rendered in parallel across multiple threads, but must produce the same output regardless of execution order (e.g., no random number generation or mutable global state). Default is true. -* **`setup()`**: Called once. **Must** be used for declaring properties (`define_property`). -* **`initialize()`**: Called when the scene is first displayed. Use for RNG seeding or state resets. `width` and `height` are accessible here. -* **`render()`**: Called every frame. **Must end with `return true`**, or the scene will terminate. - -## 3. API Reference -**Globals (Read-only, updated per frame):** -* `width`, `height`: Canvas dimensions (0-based, usually 128x128). -* `time`: Seconds elapsed since scene start/reload. -* `dt`: Delta time (seconds since last frame). - -**Drawing:** -* `set_pixel(x, y, r, g, b)`: x (0 to width-1), y (0 to height-1). Colors 0-255. Out-of-bounds coordinates are safely ignored. -* `clear()`: Rapidly fills the canvas with black (0,0,0). - -**Properties & Logging:** -* `define_property(name, type, default [, min, max])`: Types include `"float"`, `"int"`, `"bool"`, `"string"`, `"color"`. Valid *only* inside `setup()`. -* `get_property(name)`: Retrieves configured values. -* `log(message)`: Outputs to standard spdlog (INFO). - -## 4. Critical Pitfalls & Restrictions -* **No standard I/O:** `require`, `io`, `os`, and `dofile` are disabled. Only `math`, `string`, and `table` are available. -* **Type coercion:** `get_property("int")` returns a Lua double. Wrap in `math.floor()` for loops or array indexing. -* **Math.pow deprecation:** Use `^` operator instead of `math.pow` for exponentiation (e.g., `2 ^ 3` instead of `math.pow(2, 3)`). -* **Math.atan2 not existant:** Use Math.atan with two arguments instead. -* **Color extraction:** `"color"` properties return a 24-bit integer (`0xRRGGBB`). Unpack them manually: - ```lua - local raw = get_property("tint") - local r = math.floor(raw / 65536) % 256 - local g = math.floor(raw / 256) % 256 - local b = raw % 256 -``` \ No newline at end of file diff --git a/docs/LUA_SCENES.md b/docs/LUA_SCENES.md deleted file mode 100644 index 26dd544d..00000000 --- a/docs/LUA_SCENES.md +++ /dev/null @@ -1,377 +0,0 @@ -# πŸŒ™ Lua Scene Development Guide - -This document covers everything you need to write, test, and deploy custom LED-matrix -scenes in Lua β€” no C++ compilation required. - -## Table of Contents - -- [How It Works](#how-it-works) -- [Scene File Location](#scene-file-location) -- [Script Contract](#script-contract) - - [Required globals](#required-globals) - - [Lifecycle functions](#lifecycle-functions) -- [API Reference](#api-reference) - - [Read-only globals](#read-only-globals) - - [Drawing functions](#drawing-functions) - - [Property functions](#property-functions) -- [Property Types](#property-types) -- [Hot-Reload](#hot-reload) -- [Error Handling](#error-handling) -- [Examples](#examples) -- [Pitfalls & Common Mistakes](#pitfalls--common-mistakes) - ---- - -## How It Works - -The **ScriptedScenes** C++ plugin scans a directory for `.lua` files at startup. -Each file becomes its own scene that appears in the REST API and the React web UI -alongside all other C++ scenes. Existing C++ scenes are completely unchanged. - -The Lua environment is intentionally minimal: only the standard `math`, `string`, -and `table` libraries are available. There is no file I/O, no `os`, no `io`, and -no `require`. All interaction with the LED matrix goes through the functions -documented below. - ---- - -## Scene File Location - -Drop your `.lua` files into: - -``` -/lua_scenes/ -``` - -The working directory is wherever the `main` (or emulator) binary is launched from. -In a packaged install that is typically the same directory as the binary itself (e.g. -`~/led-matrix-v1.x.x/lua_scenes/`). - -If the directory does not exist it is created automatically on first run. - -The directory is **not** inside the `images/` folder β€” it lives at the same level: - -``` -led-matrix-v1.x.x/ -β”œβ”€β”€ main ← matrix binary -β”œβ”€β”€ images/ ← uploaded/processed images -β”œβ”€β”€ lua_scenes/ ← your .lua files go here βœ… -β”‚ β”œβ”€β”€ plasma.lua -β”‚ └── starfield.lua -└── plugins/ -``` - -> **Tip for development with the emulator:** The emulator is run from -> `/emulator_build/install/`, so create -> `emulator_build/install/lua_scenes/` to test your scripts locally. -> `lua` and `sol2` are installed automatically via vcpkg β€” no -> system-wide Lua installation is required. - ---- - -## Script Contract - -### Required globals - -| Global | Type | Description | -|--------|------|-------------| -| `name` | string | **Required.** Unique scene identifier used in the REST API and UI. Must not contain spaces (use underscores). | -| `external_render_only` | boolean | **Optional.** If this is a very computational heavy renderer and the Raspbeery PI will not be able to handle this on its own, set this to `true`. The scene will require a desktop (PC/Laptop) running the script | -| `offload` | boolean | **Optional.** Defaults to `true`. Set this to false, if this scene is computationally cheap and should run on the Raspberry PI itself. If offloading is enabled, the scene will fallback to render on the RPI if no desktop is connected. | -```lua -name = "my_plasma" -``` - -### Lifecycle functions - -All three functions are optional but strongly recommended. - -| Function | When called | Purpose | -|----------|-------------|---------| -| `setup()` | Once, before the first render | Declare configurable properties via `define_property()`. | -| `initialize()` | Once, when the scene is first displayed | Seed random state, pre-compute tables, reset animation state. `width` and `height` are valid here. | -| `render()` | Every frame | Draw pixels. **Must return `true`** to keep running, or `false` to signal the scene is done. | - -```lua -name = "example" - -function setup() - define_property("speed", "float", 1.0, 0.1, 5.0) -end - -function initialize() - math.randomseed(42) -end - -function render() - -- drawing code here - return true -end -``` - ---- - -## API Reference - -### Read-only globals - -These are updated by the C++ host before every call to `render()`. - -| Global | Type | Description | -|--------|------|-------------| -| `width` | integer | Matrix width in pixels (typically 128). | -| `height` | integer | Matrix height in pixels (typically 128). | -| `time` | float | Seconds elapsed since the scene started (or since the last hot-reload). | -| `dt` | float | Seconds since the previous frame. Useful for frame-rate–independent animation. | - -### Drawing functions - -#### `set_pixel(x, y, r, g, b)` - -Set one pixel. Out-of-bounds coordinates are silently ignored. - -| Parameter | Type | Range | -|-----------|------|-------| -| `x` | integer | 0 … width βˆ’ 1 (left β†’ right) | -| `y` | integer | 0 … height βˆ’ 1 (top β†’ bottom) | -| `r` | integer | 0 … 255 | -| `g` | integer | 0 … 255 | -| `b` | integer | 0 … 255 | - -Values outside 0–255 are clamped automatically. - -#### `clear()` - -Fill the entire canvas with black (0, 0, 0). Equivalent to calling -`set_pixel` on every pixel with `r=g=b=0`, but faster. - -### Property functions - -#### `define_property(name, type, default [, min, max])` - -Declare a user-configurable property. **Must only be called from `setup()`.** -Calling it from `render()` or `initialize()` has no effect. - -| Parameter | Type | Notes | -|-----------|------|-------| -| `name` | string | Unique key shown in the web UI. | -| `type` | string | One of `"float"`, `"int"`, `"bool"`, `"string"`, `"color"`. | -| `default` | any | Must match the declared type. | -| `min` | number | Optional. Only meaningful for `"float"` and `"int"`. | -| `max` | number | Optional. Only meaningful for `"float"` and `"int"`. | - -#### `get_property(name)` β†’ value - -Retrieve the current value of a property. Returns `nil` if the property name -was never registered. - -Return type depends on the property type: - -| Property type | Lua return type | -|---------------|-----------------| -| `"float"` | number (double) | -| `"int"` | number (double β€” use `math.floor` if you need an integer index) | -| `"bool"` | boolean | -| `"string"` | string | -| `"color"` | number β€” 24-bit packed RGB: `0xRRGGBB` | - -To unpack a color property: - -```lua -local raw = get_property("tint") -- e.g. 0xFF8000 -local r = math.floor(raw / 65536) % 256 -local g = math.floor(raw / 256) % 256 -local b = raw % 256 -``` - -#### `log(message)` - -Write a line to the spdlog output (INFO level). Useful for debugging. - -```lua -log("Current speed: " .. tostring(get_property("speed"))) -``` - ---- - -## Property Types - -| Type | Lua default example | Notes | -|------|---------------------|-------| -| `"float"` | `1.0` | Supports `min`/`max` constraints. | -| `"int"` | `8` | Supports `min`/`max` constraints. | -| `"bool"` | `true` | Shown as a checkbox in the web UI. | -| `"string"` | `"hello"` | Free-form text input. | -| `"color"` | `0xFF0000` | 24-bit packed RGB integer. Shown as a colour picker. | - ---- - -## Hot-Reload - -While the matrix (or emulator) is running, you can edit a `.lua` file and save it. -The C++ host checks the file's modification time at the start of every frame. -When a change is detected: - -1. The Lua state is torn down and rebuilt. -2. The script is re-executed. -3. `setup()` is called again (property *shapes* are fixed β€” adding new properties - in a reload has no effect; existing property values configured via the web UI - are preserved). -4. `initialize()` is called again. -5. The frame timer resets to `time = 0`. - -This means you can tweak your rendering algorithm and see the result in under a -second without restarting the server. - -> **Note:** Hot-reload is always active. If you want a clean state (e.g. after -> changing a property `type`), restart the matrix process. - ---- - -## Error Handling - -| Situation | Behaviour | -|-----------|-----------| -| `.lua` file has a syntax error when the server starts | Scene is **skipped** (not registered). An error is logged. | -| `setup()` throws a runtime error | Error is logged; other scenes continue normally. | -| `render()` throws a runtime error | Error is logged once per occurrence; the scene keeps running (canvas is left unchanged for that frame). | -| File disappears while running | Filesystem error is caught silently; the scene skips that frame. | - -All errors are written to the spdlog output. When running the emulator you can -see them on stdout. On the Pi, check `journalctl` or the log file if you have -configured one. - ---- - -## Examples - -Four ready-to-use scripts are included in the source tree at: - -``` -plugins/ScriptedScenes/matrix/examples/ -β”œβ”€β”€ plasma.lua – classic plasma wave with configurable speed & scale -β”œβ”€β”€ starfield.lua – 3-D star warp with configurable speed -β”œβ”€β”€ colour_bars.lua – scrolling colour bars with configurable count & speed -└── ripple.lua – concentric ripples from a bouncing centre point -``` - -Copy any of these into your `lua_scenes/` directory to get started: - -```bash -cp plugins/ScriptedScenes/matrix/examples/plasma.lua \ - emulator_build/install/lua_scenes/ -``` - -### Minimal working scene - -```lua -name = "solid_red" - -function render() - for y = 0, height - 1 do - for x = 0, width - 1 do - set_pixel(x, y, 255, 0, 0) - end - end - return true -end -``` - -### Scene with properties - -```lua -name = "pulsing_color" - -function setup() - define_property("speed", "float", 2.0, 0.1, 10.0) - define_property("tint", "color", 0x00FFFF) -end - -function render() - local raw = get_property("tint") - local tr = math.floor(raw / 65536) % 256 - local tg = math.floor(raw / 256) % 256 - local tb = raw % 256 - - local bright = (math.sin(time * get_property("speed")) + 1.0) / 2.0 - - local r = math.floor(tr * bright) - local g = math.floor(tg * bright) - local b = math.floor(tb * bright) - - for y = 0, height - 1 do - for x = 0, width - 1 do - set_pixel(x, y, r, g, b) - end - end - return true -end -``` - ---- - -## Pitfalls & Common Mistakes - -### 1. Forgetting `return true` in `render()` -If `render()` returns nothing (or `false`), the scene is considered finished and -will be rotated away immediately. Always end with `return true` unless you -intentionally want the scene to stop. - -### 2. Pixel coordinates are 0-based -`x` runs from `0` to `width - 1`; `y` from `0` to `height - 1`. Using -`for x = 1, width do` will miss column 0 and write one pixel off the right edge -(silently ignored). - -### 3. Colours are integers, not floats -`set_pixel` expects integer values 0–255. Passing a float like `127.5` will -work (it is clamped), but using `math.floor()` is clearer and avoids surprises. - -### 4. `define_property` only works inside `setup()` -Calling `define_property` from `render()` or `initialize()` is silently ignored. -All property declarations must happen in `setup()`. - -### 5. `get_property` returns a Lua `number`, not an integer -Even `int` properties come back as Lua `number` (double). If you use the value -as a table index or loop bound, wrap it with `math.floor()`: - -```lua -local count = math.floor(get_property("bar_count")) -for i = 1, count do ... end -``` - -### 6. `require`, `io`, `os`, `dofile` are not available -The Lua sandbox only exposes `base`, `math`, `string`, and `table` libraries. -There is no file I/O or module loading. If you need shared logic, copy it into -each script or use Lua's module pattern with local functions. - -### 7. Expensive per-pixel inner loops -The matrix is 128 Γ— 128 = 16 384 pixels. A plain Lua loop over every pixel -runs in interpreted bytecode and is noticeably slower than equivalent C++ code. -Keep inner loops short or reduce resolution by stepping by 2 or 4 pixels. If -you need maximum performance, write a C++ plugin instead (see -[PLUGIN_DEVELOPMENT.md](PLUGIN_DEVELOPMENT.md)). - -### 8. Hot-reload does not change property *shapes* -If you add, remove, or rename a `define_property` call and save the file, the -change takes effect for *new* scene instances (e.g. after the server restarts) -but not for the currently-running one. The live instance keeps its original -property list. Restart the server to pick up structural changes. - -### 9. `name` must be globally unique across all scenes -If two `.lua` files declare the same `name`, only one will appear in the scene -list (the last one loaded wins). Use descriptive, unique names and consider -prefixing them with `lua_` to avoid collisions with C++ scene names. - -### 10. Script not appearing after copying -The directory is scanned only at startup. Dropping a new `.lua` file into -`lua_scenes/` while the server is running has no effect until the next restart. -Hot-reload only refreshes *existing* scenes, not new files. - -### 11. Math function `math.pow` is deprecated. Use the `^` operator instead. - -```lua --- Deprecated: -local x = math.pow(2, 3) -- 8 --- Preferred: -local x = 2 ^ 3 -- 8 -``` \ No newline at end of file diff --git a/docs/PLUGIN_DEVELOPMENT.md b/docs/PLUGIN_DEVELOPMENT.md index a1cf7649..1d6492a0 100644 --- a/docs/PLUGIN_DEVELOPMENT.md +++ b/docs/PLUGIN_DEVELOPMENT.md @@ -8,7 +8,6 @@ Welcome to the LED Matrix Plugin Development Guide! This comprehensive documenta - [πŸš€ Quick Start](#-quick-start) - [πŸ“š Core APIs](#-core-apis) - [🎨 Scene Development](#-scene-development) -- [πŸŒ™ Lua Scene Development](#-lua-scene-development) - [πŸ–ΌοΈ Image Providers](#️-image-providers) - [🎭 Post-Processing Effects](#-post-processing-effects) - [🌐 REST API Integration](#-rest-api-integration) @@ -593,75 +592,6 @@ See the [Examples section](#-examples) in the main documentation for complete wo - External API integration - Real-time audio processing -## πŸŒ™ Lua Scene Development - -If you want to create custom LED-matrix scenes **without writing any C++**, the -**ScriptedScenes** plugin provides a Lua scripting layer that supports: - -- Full pixel-level rendering via `set_pixel` / `clear` -- Configurable properties exposed in the REST API and web UI -- Hot-reload β€” save a `.lua` file and the emulator picks it up within a single frame -- AI-assisted authoring through the MCP server's `write_lua_scene` / `get_frame` loop - -### Quick start - -1. Build the emulator β€” `lua` and `sol2` are pulled in automatically via the - `scripted-scenes-matrix` vcpkg feature (no system Lua installation needed): - ```bash - cmake --preset emulator && cmake --build emulator_build --target install - ``` - -2. Drop a `.lua` file into the scenes directory: - ```bash - cp plugins/ScriptedScenes/matrix/examples/plasma.lua \ - emulator_build/install/lua_scenes/ - ``` - -3. Start the emulator and `plasma` appears in `GET /list_scenes`. - -### Minimal scene - -```lua -name = "solid_red" - -function render() - for y = 0, height - 1 do - for x = 0, width - 1 do - set_pixel(x, y, 255, 0, 0) - end - end - return true -- keep running -end -``` - -### Available API - -| Symbol | Kind | Description | -|--------|------|-------------| -| `width`, `height` | global int | Matrix dimensions | -| `time` | global float | Seconds since scene start | -| `dt` | global float | Delta time since last frame | -| `set_pixel(x,y,r,g,b)` | function | Draw one pixel (0–255, clamped) | -| `clear()` | function | Fill canvas with black | -| `log(msg)` | function | Write to spdlog INFO | -| `define_property(name,type,default[,min,max])` | function | Declare a property (call from `setup()` only) | -| `get_property(name)` | function | Read a property value | - -Property types: `"float"`, `"int"`, `"bool"`, `"string"`, `"color"` (24-bit hex int). - -### Reference examples - -Four ready-to-use scripts are in `plugins/ScriptedScenes/matrix/examples/`: - -| File | Effect | -|------|--------| -| `plasma.lua` | Classic plasma wave (configurable speed & scale) | -| `starfield.lua` | 3-D star warp | -| `colour_bars.lua` | Scrolling colour bars | -| `ripple.lua` | Concentric ripples from a bouncing point | - -➑️ **Full documentation**: [docs/LUA_SCENES.md](LUA_SCENES.md) - --- ## πŸ†˜ Getting Help diff --git a/lua_scenes/.gitkeep b/lua_scenes/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/lua_scenes/aurea_borealis.lua b/lua_scenes/aurea_borealis.lua deleted file mode 100644 index de86e988..00000000 --- a/lua_scenes/aurea_borealis.lua +++ /dev/null @@ -1,165 +0,0 @@ --- aurora.lua --- Animated aurora borealis effect with flowing curtains of colour. --- Drop into lua_scenes/ and it will appear in the scene list automatically. - -name = "lua_aurora" - --- ─── Setup ──────────────────────────────────────────────────────────────────── - -function setup() - define_property("speed", "float", 1.0, 0.1, 5.0) - define_property("wave_scale", "float", 0.08, 0.01, 0.3) - define_property("bands", "int", 3, 1, 6) - define_property("color_a", "color", 0x00FF88) -- teal/green - define_property("color_b", "color", 0x4400FF) -- violet - define_property("brightness", "float", 0.85, 0.1, 1.0) -end - --- ─── Helpers ────────────────────────────────────────────────────────────────── - --- Smooth hermite interpolation (0..1 β†’ 0..1) -local function smoothstep(t) - t = math.max(0.0, math.min(1.0, t)) - return t * t * (3.0 - 2.0 * t) -end - --- Linear interpolate between two values -local function lerp(a, b, t) - return a + (b - a) * t -end - --- Unpack a 24-bit 0xRRGGBB integer into r, g, b in 0..255 -local function unpack_color(raw) - local r = math.floor(raw / 65536) % 256 - local g = math.floor(raw / 256) % 256 - local b = raw % 256 - return r, g, b -end - --- A fast, repeatable pseudo-noise function using sine sums --- Returns a value in roughly -1 .. 1 -local function noise(x, y, z) - local s = math.sin(x * 127.1 + y * 311.7 + z * 74.3) * 43758.5453 - return s - math.floor(s) -- fractional part β†’ 0..1, bias it: -end - --- Smooth multi-octave value noise (2 octaves, fast enough for 128Γ—128) -local function fbm(x, y, z) - local v = math.sin(x * 1.7 + y * 0.9 + z) -- octave 1 - + math.sin(x * 3.4 + y * 1.8 + z * 2.1) * 0.5 -- octave 2 - + math.sin(x * 6.8 + y * 3.6 + z * 3.7) * 0.25-- octave 3 - -- normalise from [-1.75, 1.75] to [0, 1] - return (v / 1.75 + 1.0) * 0.5 -end - --- ─── State ──────────────────────────────────────────────────────────────────── - -local band_offsets = {} -- per-band phase offsets for variety - -function initialize() - math.randomseed(12345) - for i = 1, 6 do - band_offsets[i] = math.random() * math.pi * 2.0 - end - log("aurora: initialized") -end - --- ─── Render ─────────────────────────────────────────────────────────────────── - -function render() - local spd = get_property("speed") - local scale = get_property("wave_scale") - local bands = math.floor(get_property("bands")) - local bright = get_property("brightness") - - local raw_a = get_property("color_a") - local raw_b = get_property("color_b") - local ar, ag, ab = unpack_color(raw_a) - local br, bg, bb = unpack_color(raw_b) - - local t = time * spd - - -- Height fraction of the matrix where the aurora lives (top 60 %) - local aurora_top = 0.0 - local aurora_bottom = 0.65 - - for y = 0, height - 1 do - local fy = y / (height - 1) -- 0 (top) β†’ 1 (bottom) - - -- Only the upper portion of the display shows aurora glow - local sky_fade = smoothstep( - (aurora_bottom - fy) / (aurora_bottom - aurora_top) - ) - - for x = 0, width - 1 do - local fx = x / (width - 1) - - -- Accumulate glow from each aurora band - local glow = 0.0 - - for i = 1, bands do - local fi = (i - 1) / math.max(bands - 1, 1) - local offset = band_offsets[i] - - -- Curtain centre wanders horizontally with time - local cx = 0.5 - + 0.3 * math.sin(t * 0.31 + offset) - + 0.15 * math.sin(t * 0.17 + offset * 1.7 + fi * 2.1) - - -- Curtain width pulses slightly - local band_w = 0.18 + 0.08 * math.sin(t * 0.23 + offset * 2.3) - - -- Horizontal envelope: Gaussian-like falloff from curtain centre - local dx = (fx - cx) / band_w - local h_shape = math.exp(-dx * dx * 2.5) - - -- Vertical ripple along the curtain (the "rays") - local ray = fbm( - fx * scale * 6.0 + offset, - fy * scale * 3.0, - t * 0.4 + offset * 0.5 - ) - - -- Bottom fade-out so curtain doesn't reach the ground harshly - local v_env = smoothstep(1.0 - fy / aurora_bottom) - * smoothstep(fy * 8.0 + 0.01) -- tiny fade at top - - glow = glow + h_shape * ray * v_env * sky_fade - end - - -- Normalise glow to 0..1 - glow = math.min(glow / bands, 1.0) * bright - - -- Colour: blend between color_a and color_b using horizontal position - -- plus a slow time shift for colour movement - local blend = fbm( - fx * 2.1 + t * 0.12, - fy * 1.3, - t * 0.07 - ) - - local r = math.floor(lerp(ar, br, blend) * glow) - local g = math.floor(lerp(ag, bg, blend) * glow) - local b = math.floor(lerp(ab, bb, blend) * glow) - - -- Add a faint star field in the dark sky above the aurora - local star = 0 - if glow < 0.05 and fy < aurora_bottom then - -- deterministic "star" based on pixel position - local sv = math.sin(x * 97.3 + y * 53.7) * 1000.0 - sv = sv - math.floor(sv) - if sv > 0.985 then - local twinkle = (math.sin(t * 3.0 + sv * 20.0) + 1.0) * 0.5 - star = math.floor(180 * twinkle) - end - end - - set_pixel(x, y, - math.max(r, star), - math.max(g, star), - math.max(b, star)) - end - end - - return true -end \ No newline at end of file diff --git a/lua_scenes/ethereal_portal.lua b/lua_scenes/ethereal_portal.lua deleted file mode 100644 index c61575ba..00000000 --- a/lua_scenes/ethereal_portal.lua +++ /dev/null @@ -1,134 +0,0 @@ --- ============================================================================ --- Scene: Ethereal Portal --- Description: A swirling, mathematically warped fluid vortex using layered --- sine waves, creating an intricate neon fractal-like pattern. --- ============================================================================ - -name = "ethereal_portal" --- external_render_only = true -- Flagged for heavy computation - --- ============================================================================ --- 1. Property Setup --- ============================================================================ -function setup() - -- Timing and structural properties - define_property("speed", "float", 1.0, 0.1, 5.0) - define_property("complexity", "int", 4, 1, 6) - define_property("zoom", "float", 3.0, 1.0, 10.0) - - -- Aesthetic palette (Returns 0xRRGGBB integers) - define_property("color_primary", "color", 0xFF0055) -- Neon Pink/Magenta - define_property("color_secondary", "color", 0x00FFFF) -- Cyan - - -- Visual modifiers - define_property("core_brightness", "float", 1.5, 0.0, 3.0) -end - --- ============================================================================ --- 2. State Initialization --- ============================================================================ -local cx, cy - -function initialize() - -- Calculate canvas center - cx = width / 2 - cy = height / 2 - - log("Ethereal Portal initialized. Canvas: " .. width .. "x" .. height) -end - --- Helper: Unpack 24-bit 0xRRGGBB integer into 0.0 to 1.0 float ranges -local function hex_to_rgb_floats(hex) - local r = (math.floor(hex / 65536) % 256) / 255.0 - local g = (math.floor(hex / 256) % 256) / 255.0 - local b = (hex % 256) / 255.0 - return r, g, b -end - --- ============================================================================ --- 3. Render Loop --- ============================================================================ -function render() - -- Safely retrieve and coerce properties - local speed = get_property("speed") - local iters = math.floor(get_property("complexity")) -- Guard against float returns - local zoom = get_property("zoom") - local core_brightness = get_property("core_brightness") - - local pr, pg, pb = hex_to_rgb_floats(get_property("color_primary")) - local sr, sg, sb = hex_to_rgb_floats(get_property("color_secondary")) - - local t = time * speed - - -- Pre-calculate rotation matrix for the vortex swirl - local cos_t = math.cos(t * 0.2) - local sin_t = math.sin(t * 0.2) - - for y = 0, height - 1 do - for x = 0, width - 1 do - -- 1. Normalize coordinates (-1.0 to 1.0) - local nx = (x - cx) / cx - local ny = (y - cy) / cy - - -- Calculate distance from center for radial falloff/vignette - local dist = math.sqrt(nx * nx + ny * ny) - - -- Apply slow global vortex rotation - local rx = (nx * cos_t - ny * sin_t) * zoom - local ry = (nx * sin_t + ny * cos_t) * zoom - - -- 2. Domain Warping / Accumulation loop - local v = 0.0 - local px, py = rx, ry - - for i = 1, iters do - local scale = i * 1.5 - - -- Shift coordinates based on underlying sine functions (Fluid dynamics illusion) - local dx = math.sin(py * scale + t) - local dy = math.cos(px * scale - t * 0.8) - - px = px + dx * 0.4 - py = py + dy * 0.4 - - -- Accumulate detail - v = v + (math.sin(px * scale) * math.cos(py * scale)) / scale - end - - -- 3. Shape the accumulated value - v = math.abs(v) -- Creates nice sharp banding lines where the waves cross 0 - v = math.min(1.0, v * 1.2) -- Boost contrast slightly - - -- Fade edges into darkness (Vignette) - local falloff = math.max(0.0, 1.0 - (dist * 0.85)) - local mix_factor = v * falloff - - -- 4. Color Mapping (Non-linear blending) - -- Black -> Primary Color -> Secondary Color - local curve1 = math.sin(mix_factor * math.pi) -- Peaks at medium intensity - local curve2 = mix_factor ^ 2.0 -- Peaks at high intensity - - local fr = (curve1 * pr) + (curve2 * sr) - local fg = (curve1 * pg) + (curve2 * sg) - local fb = (curve1 * pb) + (curve2 * sb) - - -- 5. Add an ethereal glowing core in the center - local core = math.max(0.0, 0.25 - dist) * core_brightness - core = core * (0.8 + 0.2 * math.sin(t * 4.0)) -- Pulse the core - - fr = fr + core - fg = fg + core - fb = fb + core - - -- 6. Convert back to 0-255 integer limits and draw - local out_r = math.floor(math.min(1.0, math.max(0.0, fr)) * 255) - local out_g = math.floor(math.min(1.0, math.max(0.0, fg)) * 255) - local out_b = math.floor(math.min(1.0, math.max(0.0, fb)) * 255) - - set_pixel(x, y, out_r, out_g, out_b) - end - end - - -- Mandatory return contract - return true -end diff --git a/lua_scenes/expert_quaternion_julia.lua b/lua_scenes/expert_quaternion_julia.lua deleted file mode 100644 index da6404ac..00000000 --- a/lua_scenes/expert_quaternion_julia.lua +++ /dev/null @@ -1,227 +0,0 @@ --- ============================================================================ --- THE HYPERCOMPLEX QUATERNION RAYMARCHER (v3 - Studio Polish) --- ============================================================================ --- By your resident Mathematical Visualization Maestro. --- This version implements pipeline-accurate sRGB spatial dithering to --- eliminate all banding and Moire interference patterns, resulting in a --- flawless, film-grade hypercomplex render. --- ============================================================================ - -name = "expert_quaternion_julia" --- external_render_only = true - -function setup() - -- Note: Properties only update on server restart, but the logic updates instantly! - define_property("max_steps", "int", 60, 10, 150) -- Increased default for better detail - define_property("fractal_iters", "int", 7, 2, 15) - define_property("time_scale", "float", 0.4, 0.0, 3.0) - define_property("camera_dist", "float", 2.2, 0.5, 5.0) - define_property("glow_color", "color", 0x00D9FF) -- Electric Cyan - define_property("surface_color", "color", 0xFF007A) -- Deep Pink/Magenta - define_property("light_dir_x", "float", 0.8, -1.0, 1.0) - define_property("light_dir_y", "float", 1.0, -1.0, 1.0) - define_property("light_dir_z", "float", -0.5, -1.0, 1.0) -end - -function initialize() - math.randomseed(7777777) - log("INITIALIZED EXPERT QUATERNION JULIA SET (Studio Polish Version).") -end - --- ---------------------------------------------------------------------------- --- CORE MATHEMATICAL OPERATIONS --- ---------------------------------------------------------------------------- - -local function normalize(x, y, z) - local len_sq = x*x + y*y + z*z - if len_sq == 0.0 then return 0.0, 0.0, 0.0 end - local inv_len = 1.0 / math.sqrt(len_sq) - return x * inv_len, y * inv_len, z * inv_len -end - -local function cross(ax, ay, az, bx, by, bz) - return ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx -end - --- Distance Estimator for 4D Quaternion Julia Set -local function map_distance(px, py, pz, cw, cx, cy, cz, iters) - local zw, zx, zy, zz = px, py, pz, 0.0 - local dz2 = 1.0 - local r2 = 0.0 - - for i = 1, iters do - r2 = zw*zw + zx*zx + zy*zy + zz*zz - if r2 > 15.0 then break end - - dz2 = 4.0 * r2 * dz2 - - local new_w = zw*zw - zx*zx - zy*zy - zz*zz + cw - local new_x = 2.0 * zw * zx + cx - local new_y = 2.0 * zw * zy + cy - local new_z = 2.0 * zw * zz + cz - - zw, zx, zy, zz = new_w, new_x, new_y, new_z - end - - if r2 < 1e-8 then r2 = 1e-8 end - if dz2 < 1e-8 then dz2 = 1e-8 end - - return 0.25 * math.log(r2) * math.sqrt(r2 / dz2) -end - --- ---------------------------------------------------------------------------- --- MAIN RENDER LOOP --- ---------------------------------------------------------------------------- -function render() - local max_steps = math.floor(get_property("max_steps")) - local f_iters = math.floor(get_property("fractal_iters")) - local t_scale = get_property("time_scale") - local cam_dist = get_property("camera_dist") - - local raw_glow = get_property("glow_color") - local glow_r = (math.floor(raw_glow / 65536) % 256) / 255.0 - local glow_g = (math.floor(raw_glow / 256) % 256) / 255.0 - local glow_b = (raw_glow % 256) / 255.0 - - local raw_surf = get_property("surface_color") - local surf_r = (math.floor(raw_surf / 65536) % 256) / 255.0 - local surf_g = (math.floor(raw_surf / 256) % 256) / 255.0 - local surf_b = (raw_surf % 256) / 255.0 - - local lx, ly, lz = normalize( - get_property("light_dir_x"), - get_property("light_dir_y"), - get_property("light_dir_z") - ) - - -- Animate Quaternion Seed - local sim_time = time * t_scale - local cw = math.sin(sim_time * 0.45) * 0.45 - local cx = math.cos(sim_time * 0.32) * 0.45 - local cy = math.sin(sim_time * 0.71) * 0.45 - local cz = math.cos(sim_time * 0.19) * 0.45 - - -- Camera Matrix Setup - local cam_x = math.sin(sim_time * 0.2) * cam_dist - local cam_y = math.sin(sim_time * 0.15) * (cam_dist * 0.6) - local cam_z = math.cos(sim_time * 0.2) * cam_dist - - local fw_x, fw_y, fw_z = normalize(-cam_x, -cam_y, -cam_z) - local rt_x, rt_y, rt_z = normalize(cross(fw_x, fw_y, fw_z, 0.0, 1.0, 0.0)) - local up_x, up_y, up_z = cross(rt_x, rt_y, rt_z, fw_x, fw_y, fw_z) - - local half_w, half_h = width * 0.5, height * 0.5 - local inv_h = 1.0 / height - local zoom = 1.2 - local hit_threshold = 0.002 - local normal_eps = 0.002 - - clear() - - for y = 0, height - 1 do - local uv_y = -(y - half_h) * inv_h * 2.0 - - for x = 0, width - 1 do - local uv_x = (x - half_w) * inv_h * 2.0 - - local rd_x = fw_x * zoom + rt_x * uv_x + up_x * uv_y - local rd_y = fw_y * zoom + rt_y * uv_x + up_y * uv_y - local rd_z = fw_z * zoom + rt_z * uv_x + up_z * uv_y - rd_x, rd_y, rd_z = normalize(rd_x, rd_y, rd_z) - - local t = 0.0 - local dist = 0.0 - local glow_accum = 0.0 - local hit = false - local px, py, pz = 0.0, 0.0, 0.0 - - for s = 1, max_steps do - px = cam_x + rd_x * t - py = cam_y + rd_y * t - pz = cam_z + rd_z * t - - dist = map_distance(px, py, pz, cw, cx, cy, cz, f_iters) - - -- Accumulate volumetric glow - glow_accum = glow_accum + 0.03 / (1.0 + dist * dist * 40.0) - - if dist < hit_threshold then - hit = true - break - end - - t = t + dist - if t > 6.0 then break end - end - - local final_r, final_g, final_b = 0.0, 0.0, 0.0 - - if hit then - local nx = map_distance(px + normal_eps, py, pz, cw, cx, cy, cz, f_iters) - - map_distance(px - normal_eps, py, pz, cw, cx, cy, cz, f_iters) - local ny = map_distance(px, py + normal_eps, pz, cw, cx, cy, cz, f_iters) - - map_distance(px, py - normal_eps, pz, cw, cx, cy, cz, f_iters) - local nz = map_distance(px, py, pz + normal_eps, cw, cx, cy, cz, f_iters) - - map_distance(px, py, pz - normal_eps, cw, cx, cy, cz, f_iters) - nx, ny, nz = normalize(nx, ny, nz) - - local ao = 1.0 - (t / 6.0) - if ao < 0.0 then ao = 0.0 end - - local diff = nx*lx + ny*ly + nz*lz - if diff < 0.0 then diff = 0.0 end - - local dot_rd_n = rd_x*nx + rd_y*ny + rd_z*nz - local rx = rd_x - 2.0 * dot_rd_n * nx - local ry = rd_y - 2.0 * dot_rd_n * ny - local rz = rd_z - 2.0 * dot_rd_n * nz - - local spec = rx*lx + ry*ly + rz*lz - if spec < 0.0 then spec = 0.0 end - spec = spec ^ 24.0 - - final_r = (surf_r * diff * ao) + (spec * 0.8) - final_g = (surf_g * diff * ao) + (spec * 0.8) - final_b = (surf_b * diff * ao) + (spec * 0.8) - end - - -- Add Nebula Glow - final_r = final_r + glow_accum * glow_r * 0.8 - final_g = final_g + glow_accum * glow_g * 0.8 - final_b = final_b + glow_accum * glow_b * 0.8 - - -- Prevent negative values before Tonemapping - final_r = math.max(0.0, final_r) - final_g = math.max(0.0, final_g) - final_b = math.max(0.0, final_b) - - -- Tone Mapping (Reinhard) - final_r = final_r / (1.0 + final_r) - final_g = final_g / (1.0 + final_g) - final_b = final_b / (1.0 + final_b) - - -- Gamma Correction (Linear -> sRGB) - final_r = math.sqrt(final_r) - final_g = math.sqrt(final_g) - final_b = math.sqrt(final_b) - - -- ========================================================= - -- THE FIX: Industry Standard Spatial Dithering applied - -- *after* Gamma correction to prevent value blow-out. - -- ========================================================= - local noise = ((math.sin(x * 12.9898 + y * 78.233) * 43758.5453) % 1.0 - 0.5) * 0.015 - final_r = final_r + noise - final_g = final_g + noise - final_b = final_b + noise - - -- Strict clamp & cast to 8-bit color - local out_r = math.floor(math.max(0.0, math.min(1.0, final_r)) * 255) - local out_g = math.floor(math.max(0.0, math.min(1.0, final_g)) * 255) - local out_b = math.floor(math.max(0.0, math.min(1.0, final_b)) * 255) - - set_pixel(x, y, out_r, out_g, out_b) - end - end - - return true -end \ No newline at end of file diff --git a/lua_scenes/fire.lua b/lua_scenes/fire.lua deleted file mode 100644 index 8d0cad21..00000000 --- a/lua_scenes/fire.lua +++ /dev/null @@ -1,164 +0,0 @@ --- fire.lua --- Classic "DOOM-style" fire simulation on the LED matrix. --- A heat buffer propagates upward each frame, cooling as it rises. --- Drop into lua_scenes/ and restart the server. - -name = "lua_fire" -offload = true - --- ─── Setup ──────────────────────────────────────────────────────────────────── - -function setup() - define_property("intensity", "float", 0.92, 0.5, 1.0) - define_property("cooling", "float", 0.08, 0.01, 0.4) - define_property("wind", "float", 0.0, -2.0, 2.0) - define_property("color_mode", "int", 0, 0, 2) - -- 0 = classic fire (black β†’ red β†’ orange β†’ yellow β†’ white) - -- 1 = cold fire (black β†’ blue β†’ cyan β†’ white) - -- 2 = toxic (black β†’ green β†’ yellow β†’ white) -end - --- ─── State ──────────────────────────────────────────────────────────────────── - -local heat = {} -- flat [y * width + x] heat buffer, values 0.0 .. 1.0 -local W, H -- cached dimensions - --- Build a flat index (0-based x/y) -local function idx(x, y) - return y * W + x + 1 -- Lua tables are 1-based -end - --- ─── Colour palette ─────────────────────────────────────────────────────────── - --- Map heat 0..1 to r,g,b using a 4-stop gradient -local palettes = { - -- classic fire: black β†’ red β†’ orange β†’ yellow β†’ white - [0] = { - {0, 0, 0}, -- 0.0 black - {180, 0, 0}, -- 0.35 deep red - {255, 80, 0}, -- 0.6 orange - {255, 220, 30}, -- 0.85 yellow - {255, 255, 255}, -- 1.0 white - }, - -- cold fire: black β†’ deep blue β†’ cyan β†’ white - [1] = { - {0, 0, 0}, - {0, 0, 200}, - {0, 120, 255}, - {80, 220, 255}, - {255, 255, 255}, - }, - -- toxic: black β†’ dark green β†’ acid green β†’ yellow β†’ white - [2] = { - {0, 0, 0}, - {0, 80, 0}, - {40, 200, 0}, - {180, 255, 40}, - {255, 255, 255}, - }, -} - -local stops = {0.0, 0.35, 0.6, 0.85, 1.0} - -local function heat_to_rgb(h, mode) - local pal = palettes[mode] or palettes[0] - -- find the two surrounding stops - local lo, hi = 1, 2 - for i = 2, #stops do - if h <= stops[i] then - lo = i - 1 - hi = i - break - end - lo = #stops - 1 - hi = #stops - end - local t = 0.0 - local denom = stops[hi] - stops[lo] - if denom > 0 then - t = (h - stops[lo]) / denom - end - local ca = pal[lo] - local cb = pal[hi] - local r = math.floor(ca[1] + (cb[1] - ca[1]) * t) - local g = math.floor(ca[2] + (cb[2] - ca[2]) * t) - local b = math.floor(ca[3] + (cb[3] - ca[3]) * t) - return r, g, b -end - --- ─── Lifecycle ──────────────────────────────────────────────────────────────── - -function initialize() - W = width - H = height - -- zero out heat buffer - for i = 1, W * H do - heat[i] = 0.0 - end - log("lua_fire: initialized " .. W .. "x" .. H) -end - --- ─── Render ─────────────────────────────────────────────────────────────────── - -function render() - local intensity = get_property("intensity") - local cooling = get_property("cooling") - local wind = get_property("wind") - local mode = math.floor(get_property("color_mode")) - - -- 1. Seed the bottom row with hot coals (randomised each frame) - local base_y = H - 1 - for x = 0, W - 1 do - -- Random embers: some cells max-heat, some flicker off - local v = math.random() - if v < intensity then - heat[idx(x, base_y)] = 0.85 + math.random() * 0.15 - else - heat[idx(x, base_y)] = heat[idx(x, base_y)] * 0.8 - end - end - - -- 2. Also seed the second-to-last row for a thicker base - for x = 0, W - 1 do - if math.random() < intensity * 0.7 then - heat[idx(x, base_y - 1)] = - (heat[idx(x, base_y - 1)] + heat[idx(x, base_y)]) * 0.5 - end - end - - -- 3. Propagate heat upward (iterate top-down so we don't re-use new values) - -- Wind shifts the sample point horizontally by Β±1 pixel on average. - local wind_bias = math.floor(wind + 0.5) -- -2..2 integer pixel shift - - for y = 0, H - 3 do - for x = 0, W - 1 do - -- Average heat from three cells one row below, shifted by wind - local sum = 0.0 - local cnt = 0 - for dx = -1, 1 do - local sx = x + dx + wind_bias - if sx >= 0 and sx < W then - sum = sum + heat[idx(sx, y + 1)] - cnt = cnt + 1 - end - end - - local avg = (cnt > 0) and (sum / cnt) or 0.0 - - -- Cool down as heat rises - local cool = math.random() * cooling - heat[idx(x, y)] = math.max(0.0, avg - cool) - end - end - - -- 4. Draw heat buffer to pixels - for y = 0, H - 1 do - for x = 0, W - 1 do - local h = heat[idx(x, y)] - local r, g, b = heat_to_rgb(h, mode) - set_pixel(x, y, r, g, b) - end - end - - return true -end \ No newline at end of file diff --git a/lua_scenes/hyperdimensional_manifold.lua b/lua_scenes/hyperdimensional_manifold.lua deleted file mode 100644 index ced8f7e4..00000000 --- a/lua_scenes/hyperdimensional_manifold.lua +++ /dev/null @@ -1,208 +0,0 @@ -name = "hyperdimensional_manifold" -external_render_only = true - --- Cache math functions locally for maximum performance during the heavy raymarching loop -local sin, cos, sqrt, abs, max, min, floor, exp = math.sin, math.cos, math.sqrt, math.abs, math.max, math.min, math -.floor, math.exp - -function setup() - -- Expert-level property exposure for live manipulation - define_property("time_scale", "float", 0.6, 0.1, 2.0) - define_property("camera_zoom", "float", 1.2, 0.5, 3.0) - define_property("glow_yield", "float", 1.8, 0.1, 5.0) - define_property("max_steps", "int", 35, 10, 80) - - -- The core quantum emission frequency (base color) - -- Default is a hyper-cyan/teal - define_property("emission_color", "color", 0x00FFBB) -end - -function initialize() - log("Hyperdimensional Manifold Initialized.") - log("Compiling Volumetric SDFs and Non-Euclidean Camera Matrices...") -end - --- Polynomial Smooth Maximum --- Used to carve fluid, organic intersections between rigid geometric SDFs -local function smax(a, b, k) - local h = max(k - abs(a - b), 0.0) / k - return max(a, b) + h * h * k * 0.25 -end - --- The Signed Distance Field (SDF) of the universe --- Maps a 3D point (x,y,z) to the nearest distance of our mathematical geometry -local function map(x, y, z, t) - -- 1. Infinite Domain Repetition along Z axis - local spacing = 5.0 - local lz = (z + spacing * 0.5) % spacing - spacing * 0.5 - - -- 2. Inverted Cylinder (Forms the base infinite tunnel bounding the universe) - local tunnel_radius = 1.8 + sin(z * 0.5 + t) * 0.3 - local cyl = tunnel_radius - sqrt(x * x + y * y) - - -- 3. KIFS Folded Octahedrons - -- We fold space across all 3 axis symmetrically - local fx, fy, fz = abs(x), abs(y), abs(lz) - -- Distance to an octahedron centered at the repeating origin - local oct = (fx + fy + fz - 2.5) * 0.57735027 - - -- 4. High-frequency Gyroid Manifold - -- This adds the intricate, organic "alien webbing" displacement - local scale = 3.0 - local g = (sin(x * scale) * cos(y * scale) + sin(y * scale) * cos(z * scale) + sin(z * scale) * cos(x * scale)) / - scale - - -- 5. Constructive Solid Geometry (CSG) - -- We elegantly carve the KIFS octahedrons OUT of the solid tunnel using smooth maximum, - -- then displace the resulting manifold with our gyroid function. - local hollow = smax(cyl, -oct, 1.2) - - -- Return final distance (scaled slightly to prevent ray-stepping artifacts across the Lipschitz boundary) - return hollow - g * 0.4 -end - -function render() - -- Map properties - local t = time * get_property("time_scale") - local zoom = get_property("camera_zoom") - local glow_yield = get_property("glow_yield") - local steps = floor(get_property("max_steps")) - - -- Unpack true 24-bit color into normalized RGB vectors [0.0 - 1.0] - local raw_color = get_property("emission_color") - local base_r = (floor(raw_color / 65536) % 256) / 255.0 - local base_g = (floor(raw_color / 256) % 256) / 255.0 - local base_b = (raw_color % 256) / 255.0 - - local aspect = width / height - - -- ========================================== - -- CAMERA KINEMATICS (Orthogonal Basis Matrix) - -- ========================================== - -- Dynamic swaying camera path flying through the domain - local cam_x = sin(t * 0.6) * 0.8 - local cam_y = cos(t * 0.4) * 0.8 - local cam_z = t * 2.5 - - -- The camera's "look at" target, slightly ahead on the curve - local target_x = sin((t + 0.5) * 0.6) * 0.8 - local target_y = cos((t + 0.5) * 0.4) * 0.8 - local target_z = cam_z + 1.0 - - -- Forward Vector (Z) - local fw_x, fw_y, fw_z = target_x - cam_x, target_y - cam_y, target_z - cam_z - local fw_len = sqrt(fw_x * fw_x + fw_y * fw_y + fw_z * fw_z) - fw_x, fw_y, fw_z = fw_x / fw_len, fw_y / fw_len, fw_z / fw_len - - -- Dynamic Roll (Banking the camera as it turns) - local roll = sin(t * 0.5) * 0.6 - local su, cu = sin(roll), cos(roll) - local up_x, up_y, up_z = -su, cu, 0.0 - - -- Right Vector (X) = Cross(Forward, Up) - local r_x = up_y * fw_z - up_z * fw_y - local r_y = up_z * fw_x - up_x * fw_z - local r_z = up_x * fw_y - up_y * fw_x - local r_len = sqrt(r_x * r_x + r_y * r_y + r_z * r_z) - r_x, r_y, r_z = r_x / r_len, r_y / r_len, r_z / r_len - - -- True Up Vector (Y) = Cross(Right, Forward) - local u_x = fw_y * r_z - fw_z * r_y - local u_y = fw_z * r_x - fw_x * r_z - local u_z = fw_x * r_y - fw_y * r_x - - -- ========================================== - -- RAYMARCHING ENGINE - -- ========================================== - for py = 0, height - 1 do - for px = 0, width - 1 do - -- Transform pixel into Normalized Device Coordinates [-1.0 to 1.0] - local uv_x = (px / width * 2.0 - 1.0) * aspect - local uv_y = (py / height * 2.0 - 1.0) - - -- Construct ray direction by multiplying UVs by the Camera Basis Matrix - local rd_x = uv_x * r_x + uv_y * u_x + fw_x * zoom - local rd_y = uv_x * r_y + uv_y * u_y + fw_y * zoom - local rd_z = uv_x * r_z + uv_y * u_z + fw_z * zoom - - -- Normalize Ray Direction - local rd_len = sqrt(rd_x * rd_x + rd_y * rd_y + rd_z * rd_z) - rd_x, rd_y, rd_z = rd_x / rd_len, rd_y / rd_len, rd_z / rd_len - - -- Raymarching states - local total_dist = 0.0 - local accum_glow_r = 0.0 - local accum_glow_g = 0.0 - local accum_glow_b = 0.0 - local hit = false - - -- Step the ray through the vector field - for i = 1, steps do - local p_x = cam_x + rd_x * total_dist - local p_y = cam_y + rd_y * total_dist - local p_z = cam_z + rd_z * total_dist - - -- Sample the universe's distance field - local dist = map(p_x, p_y, p_z, t) - - -- ========================================== - -- VOLUMETRIC INTEGRATION - -- ========================================== - -- As rays graze surfaces, they accumulate energy. - -- We use an exponential falloff based on the distance to create a pseudo-scattered plasma glow. - -- We offset the color channels spatially to simulate chromatic aberration / iridescence. - local density = exp(-dist * 4.5) - accum_glow_r = accum_glow_r + density * (0.6 + 0.4 * sin(p_z * 1.0 + t)) * base_r * 0.06 - accum_glow_g = accum_glow_g + density * (0.6 + 0.4 * sin(p_z * 1.1 + t)) * base_g * 0.06 - accum_glow_b = accum_glow_b + density * (0.6 + 0.4 * sin(p_z * 1.2 + t)) * base_b * 0.06 - - -- Check for solid hit or infinity escape - if dist < 0.002 then - hit = true - break - end - if total_dist > 15.0 then - break - end - - -- Step forward (multiplied by a safety factor to prevent tunneling artifacts) - total_dist = total_dist + dist * 0.6 - end - - -- Combine volumetric scatter with base colors - local final_r = accum_glow_r * glow_yield - local final_g = accum_glow_g * glow_yield - local final_b = accum_glow_b * glow_yield - - -- Add fake ambient occlusion based on ray distance if we hit geometry - if hit then - local ao = 1.0 / (1.0 + total_dist * total_dist * 0.05) - final_r = final_r + ao * base_r * 0.2 - final_g = final_g + ao * base_g * 0.2 - final_b = final_b + ao * base_b * 0.2 - end - - -- ========================================== - -- POST-PROCESSING (Reinhard Tone Mapping & Gamma) - -- ========================================== - -- Smoothly map infinitely bright HDR values back into a 0.0-1.0 range - final_r = final_r / (final_r + 1.0) - final_g = final_g / (final_g + 1.0) - final_b = final_b / (final_b + 1.0) - - -- Gamma Correction approximation (1.0/2.2 ~ 0.5) - final_r = sqrt(final_r) - final_g = sqrt(final_g) - final_b = sqrt(final_b) - - -- Quantize to 8-bit color space and protect against bounds - local r_out = floor(max(0, min(255, final_r * 255))) - local g_out = floor(max(0, min(255, final_g * 255))) - local b_out = floor(max(0, min(255, final_b * 255))) - - set_pixel(px, py, r_out, g_out, b_out) - end - end - - return true -end diff --git a/lua_scenes/mobius_fluid_manifold.lua b/lua_scenes/mobius_fluid_manifold.lua deleted file mode 100644 index 6d772863..00000000 --- a/lua_scenes/mobius_fluid_manifold.lua +++ /dev/null @@ -1,186 +0,0 @@ -name = "mobius_fluid_manifold" - -function setup() - -- Mathematical parameters - define_property("zoom", "float", 1.2, 0.1, 10.0) - define_property("speed", "float", 0.5, 0.0, 5.0) - define_property("iterations", "int", 5.0, 1.0, 10.0) - - -- Aesthetic properties - define_property("tint_base", "color", 0x0A0F24) -- Deep abyss blue - define_property("tint_peak", "color", 0xFF0055) -- Neon pink - define_property("iridescence", "float", 1.2, 0.0, 3.0) - define_property("light_height", "float", 1.5, 0.1, 5.0) -end - -function initialize() - log("Mobius Fluid Manifold initialized.") - log("Canvas: " .. width .. "x" .. height .. " | Executing analytical PBR math...") -end - --- ============================================================================ --- CORE MATHEMATICS --- ============================================================================ - --- Iterated Function System (IFS) to generate organic heightfield manifold -local function evaluate_manifold(x, y, t, iters) - local h = 0.0 - local amp = 1.0 - local freq = 1.0 - - -- Precalculated rotation matrix coefficients for domain warping - local rot_cos = math.cos(0.5) - local rot_sin = math.sin(0.5) - - for i = 1, iters do - -- Intersecting sine waves - h = h + amp * math.abs(math.sin(x * freq + t) * math.cos(y * freq - t)) - - -- Domain warp & rotate coordinates for the next octave - local nx = x * rot_cos - y * rot_sin - local ny = x * rot_sin + y * rot_cos - - -- Non-linear space offset - x = nx + math.sin(y * freq + t) * 0.4 - y = ny + math.cos(x * freq - t) * 0.4 - - -- Fractal scaling - freq = freq * 1.9 - amp = amp * 0.55 - end - return h -end - --- Fast color clamp helper -local function clamp_color(v) - if v < 0.0 then return 0 end - if v > 255.0 then return 255 end - return math.floor(v) -end - --- ============================================================================ --- MAIN RENDER LOOP --- ============================================================================ - -function render() - -- 1. Cache properties outside the per-pixel loop for performance - local zoom = get_property("zoom") - local speed = get_property("speed") - local iters = math.floor(get_property("iterations")) -- Ensure int is a proper integer - local irid = get_property("iridescence") - local lh = get_property("light_height") - - -- 2. Unpack 24-bit Colors - local c_base = get_property("tint_base") - local br = math.floor(c_base / 65536) % 256 - local bg = math.floor(c_base / 256) % 256 - local bb = c_base % 256 - - local c_peak = get_property("tint_peak") - local pr = math.floor(c_peak / 65536) % 256 - local pg = math.floor(c_peak / 256) % 256 - local pb = c_peak % 256 - - -- 3. Calculate Global Scene State - local t = time * speed - local aspect = width / height - - -- Precompute complex coefficients for MΓΆbius Transformation: f(z) = (az + b) / (cz + d) - local ar, ai = math.cos(t * 0.3), math.sin(t * 0.4) - local br_c, bi_c = 0.5 * math.sin(t * 0.7), 0.5 * math.cos(t * 0.2) - local cr, ci = math.sin(t * 0.5), -math.cos(t * 0.6) - local dr, di = 0.8 * math.cos(t * 0.1), 0.8 * math.sin(t * 0.8) - - -- Precompute light direction (Orbiting point light) - local lx = math.sin(t * 0.5) - local ly = math.cos(t * 0.5) - local lz = lh - local llen = math.sqrt(lx*lx + ly*ly + lz*lz) - lx, ly, lz = lx/llen, ly/llen, lz/llen - - -- View vector (looking straight down into the screen) - local vx, vy, vz = 0.0, 0.0, 1.0 - - -- Precalculate Half-way vector for Blinn-Phong Specular - local h_x, h_y, h_z = lx + vx, ly + vy, lz + vz - local h_len = math.sqrt(h_x*h_x + h_y*h_y + h_z*h_z) - h_x, h_y, h_z = h_x/h_len, h_y/h_len, h_z/h_len - - -- 4. Per-Pixel Rendering - for y = 0, height - 1 do - -- Normalize Y to [-1, 1] - local uv_y = (y / height - 0.5) * 2.0 * zoom - - for x = 0, width - 1 do - -- Normalize X to [-1, 1] with Aspect Ratio correction - local uv_x = (x / width - 0.5) * 2.0 * aspect * zoom - - -- --- A. COMPLEX SPACE WARPING (MΓΆbius Transform) --- - -- Complex multiplication and addition inlined for extreme performance - -- num = a*z + b - local num_r = ar*uv_x - ai*uv_y + br_c - local num_i = ar*uv_y + ai*uv_x + bi_c - -- den = c*z + d - local den_r = cr*uv_x - ci*uv_y + dr - local den_i = cr*uv_y + ci*uv_x + di - - -- Complex division: z' = num / den - local den_mag = den_r*den_r + den_i*den_i + 1e-5 -- Prevent divide by zero - local mx = (num_r*den_r + num_i*den_i) / den_mag - local my = (num_i*den_r - num_r*den_i) / den_mag - - -- --- B. ANALYTICAL GRADIENTS --- - -- We sample the heightmap at microscopic offsets to calculate normals - local eps = 0.01 - local h = evaluate_manifold(mx, my, t, iters) - local hx = evaluate_manifold(mx + eps, my, t, iters) - local hy = evaluate_manifold(mx, my + eps, t, iters) - - -- Finite difference to get slope - local dx = (hx - h) / eps - local dy = (hy - h) / eps - local dz = 2.0 -- Controls surface bumpiness/steepness - - -- Normalize normal vector - local len = math.sqrt(dx*dx + dy*dy + dz*dz) - local nx, ny, nz = dx/len, dy/len, dz/len - - -- --- C. ILLUMINATION MODEL --- - -- Diffuse lighting - local ndotl = math.max(0.0, nx*lx + ny*ly + nz*lz) - - -- Specular highlight (Blinn-Phong) - local ndoth = math.max(0.0, nx*h_x + ny*h_y + nz*h_z) - local spec = ndoth ^ 64.0 - - -- Fresnel Reflectance (Schlick's approximation) - local ndotv = math.max(0.0, nx*vx + ny*vy + nz*vz) - local fresnel = (1.0 - ndotv) ^ 3.0 - - -- --- D. COLOR SYNTHESIS --- - -- Blend primary/secondary colors based on elevation and grazing angle (fresnel) - local mix = math.min(1.0, math.max(0.0, h * 0.6 + fresnel * irid)) - - local out_r = br + (pr - br) * mix - local out_g = bg + (pg - bg) * mix - local out_b = bb + (pb - bb) * mix - - -- Phase shifting iridescence based on domain cross-product (creates rainbow oil-slick effect) - local phase = mx * ny - my * nx + t - out_r = out_r + math.sin(phase * 4.0) * 30.0 * irid - out_g = out_g + math.sin(phase * 4.0 + 2.0) * 30.0 * irid - out_b = out_b + math.sin(phase * 4.0 + 4.0) * 30.0 * irid - - -- Apply Lighting (Diffuse attenuation + Additive Specular highlight) - out_r = out_r * (0.3 + 0.7 * ndotl) + spec * 255.0 - out_g = out_g * (0.3 + 0.7 * ndotl) + spec * 255.0 - out_b = out_b * (0.3 + 0.7 * ndotl) + spec * 255.0 - - -- Commit to screen - set_pixel(x, y, clamp_color(out_r), clamp_color(out_g), clamp_color(out_b)) - end - end - - -- Required to keep the scene active - return true -end \ No newline at end of file diff --git a/lua_scenes/neon_orbit_tunnel.lua b/lua_scenes/neon_orbit_tunnel.lua deleted file mode 100644 index 370d2b48..00000000 --- a/lua_scenes/neon_orbit_tunnel.lua +++ /dev/null @@ -1,187 +0,0 @@ -name = "neon_orbit_tunnel" - -local stars = {} -local palette = {} -local center_x = 0 -local center_y = 0 - -local function clamp(v) - if v < 0 then - return 0 - elseif v > 255 then - return 255 - end - return math.floor(v) -end - -local function hsv_to_rgb(h, s, v) - local i = math.floor(h * 6) - local f = h * 6 - i - local p = v * (1 - s) - local q = v * (1 - f * s) - local t = v * (1 - (1 - f) * s) - - local r, g, b - - i = i % 6 - - if i == 0 then - r, g, b = v, t, p - elseif i == 1 then - r, g, b = q, v, p - elseif i == 2 then - r, g, b = p, v, t - elseif i == 3 then - r, g, b = p, q, v - elseif i == 4 then - r, g, b = t, p, v - else - r, g, b = v, p, q - end - - return clamp(r * 255), clamp(g * 255), clamp(b * 255) -end - -function setup() - define_property("rotation_speed", "float", 1.2, 0.1, 8.0) - define_property("tunnel_depth", "float", 2.5, 0.5, 8.0) - define_property("star_count", "int", 90, 10, 240) - define_property("trail_strength", "float", 0.85, 0.5, 0.98) - define_property("rainbow_mode", "bool", true) - define_property("base_color", "color", 0x00D4FF) -end - -function initialize() - math.randomseed(1337) - - center_x = width / 2 - center_y = height / 2 - - palette = {} - - for i = 0, 255 do - local r, g, b = hsv_to_rgb(i / 255.0, 1.0, 1.0) - palette[i] = { r = r, g = g, b = b } - end - - stars = {} - - local count = math.floor(get_property("star_count")) - - for i = 1, count do - stars[i] = { - angle = math.random() * math.pi * 2, - radius = math.random(), - speed = 0.2 + math.random() * 1.8, - twist = 0.5 + math.random() * 2.0, - brightness = 0.4 + math.random() * 0.6 - } - end - - log("Initialized neon_orbit_tunnel with " .. tostring(count) .. " stars") -end - -local function unpack_color(raw) - local r = math.floor(raw / 65536) % 256 - local g = math.floor(raw / 256) % 256 - local b = raw % 256 - return r, g, b -end - -function render() - local trail = get_property("trail_strength") - local rotation_speed = get_property("rotation_speed") - local tunnel_depth = get_property("tunnel_depth") - local rainbow_mode = get_property("rainbow_mode") - - local base_r, base_g, base_b = unpack_color(get_property("base_color")) - - clear() - - local ring_count = 18 - - for ring = 1, ring_count do - local z = (ring / ring_count) - local pulse = 0.5 + 0.5 * math.sin(time * 1.4 + z * 12) - - local radius = (z ^ 1.8) * width * 0.55 - local rotation = time * rotation_speed * (0.2 + z) - - for segment = 0, 95 do - local a = (segment / 96.0) * math.pi * 2 + rotation - - local wobble = math.sin(a * 3 + time * 2 + z * 8) * 4 - - local x = math.floor(center_x + math.cos(a) * (radius + wobble)) - local y = math.floor(center_y + math.sin(a) * (radius + wobble)) - - local intensity = (1.0 - z) * pulse * trail - - local r, g, b - - if rainbow_mode then - local color_index = math.floor((segment * 2 + time * 80 + z * 255) % 255) - local c = palette[color_index] - - r = clamp(c.r * intensity) - g = clamp(c.g * intensity) - b = clamp(c.b * intensity) - else - r = clamp(base_r * intensity) - g = clamp(base_g * intensity) - b = clamp(base_b * intensity) - end - - set_pixel(x, y, r, g, b) - end - end - - for i = 1, #stars do - local star = stars[i] - - star.radius = star.radius + dt * star.speed * tunnel_depth * 0.15 - star.angle = star.angle + dt * rotation_speed * star.twist - - if star.radius > 1.2 then - star.radius = 0.05 - star.angle = math.random() * math.pi * 2 - end - - local spiral = star.radius * width * 0.7 - - local x = math.floor(center_x + math.cos(star.angle) * spiral) - local y = math.floor(center_y + math.sin(star.angle) * spiral) - - local brightness = (1.2 - star.radius) * 255 * star.brightness - - local r, g, b - - if rainbow_mode then - local color_index = math.floor((star.angle * 40 + time * 120) % 255) - local c = palette[color_index] - - r = clamp(c.r * brightness / 255) - g = clamp(c.g * brightness / 255) - b = clamp(c.b * brightness / 255) - else - r = clamp(base_r * brightness / 255) - g = clamp(base_g * brightness / 255) - b = clamp(base_b * brightness / 255) - end - - set_pixel(x, y, r, g, b) - - local tail_x = math.floor(center_x + math.cos(star.angle - 0.08) * (spiral - 2)) - local tail_y = math.floor(center_y + math.sin(star.angle - 0.08) * (spiral - 2)) - - set_pixel( - tail_x, - tail_y, - clamp(r * 0.4), - clamp(g * 0.4), - clamp(b * 0.4) - ) - end - - return true -end \ No newline at end of file diff --git a/lua_scenes/plasma_waves.lua b/lua_scenes/plasma_waves.lua deleted file mode 100644 index 60602931..00000000 --- a/lua_scenes/plasma_waves.lua +++ /dev/null @@ -1,88 +0,0 @@ --- ============================================================================ --- File: plasma_waves.lua --- Location: /lua_scenes/plasma_waves.lua --- ============================================================================ - --- 1. Script Contract: Required unique global name (no spaces) -name = "plasma_waves" - --- 2. Setup: Called once. Strictly used for defining properties. -function setup() - -- define_property(name, type, default [, min, max]) - define_property("speed", "float", 2.0, 0.1, 10.0) - define_property("scale", "float", 15.0, 1.0, 50.0) - - -- Int property (will need math.floor during extraction) - define_property("complexity", "int", 3, 1, 4) - - -- Color property (defaults to Cyan: 0x00FFFF) - define_property("tint", "color", 0x00FFFF) -end - --- 3. Initialize: Called when first displayed. -function initialize() - -- Good place to log setup and reset states - log("Plasma Waves initialized! Canvas size: " .. width .. "x" .. height) -end - --- 4. Render: Called every frame. Must return true. -function render() - -- Rapidly fill the canvas with black - clear() - - -- Retrieve properties - local speed = get_property("speed") - local scale = get_property("scale") - - -- Pitfall Prevention: Force int property into a Lua integer - local complexity = math.floor(get_property("complexity")) - - -- Pitfall Prevention: Unpack the 24-bit color integer manually - local raw_color = get_property("tint") - local base_r = math.floor(raw_color / 65536) % 256 - local base_g = math.floor(raw_color / 256) % 256 - local base_b = raw_color % 256 - - -- Calculate current animation phase based on elapsed time and speed - local t = time * speed - - -- Loop through every pixel (0 to width-1, 0 to height-1) - for x = 0, width - 1 do - for y = 0, height - 1 do - - -- Coordinate math based on scale - local nx = x / scale - local ny = y / scale - - -- Accumulate sine waves for a classic "plasma" effect - local v = 0 - - -- Layer 1 & 2: Simple moving directional waves - if complexity >= 1 then v = v + math.sin(nx + t) end - if complexity >= 2 then v = v + math.sin(ny - t) end - -- Layer 3: Diagonal wave - if complexity >= 3 then v = v + math.sin((nx + ny + t) / 2.0) end - -- Layer 4: Circular ripples from the origin - if complexity >= 4 then v = v + math.sin(math.sqrt(nx*nx + ny*ny) - t) end - - -- Normalize the accumulated sine value (roughly between 0.0 and 1.0) - local normalized = (v + complexity) / (complexity * 2.0) - - -- Modulate the user's chosen base color using the math - local r = math.floor(base_r * normalized) - local g = math.floor(base_g * math.abs(math.sin(normalized * math.pi))) - local b = math.floor(base_b * (1.0 - normalized)) - - -- Safely clamp colors between 0 and 255 - r = math.max(0, math.min(255, r)) - g = math.max(0, math.min(255, g)) - b = math.max(0, math.min(255, b)) - - -- Draw the pixel - set_pixel(x, y, r, g, b) - end - end - - -- Required: Must end with return true or the scene will terminate - return true -end \ No newline at end of file diff --git a/lua_scenes/quantum_accretion.lua b/lua_scenes/quantum_accretion.lua deleted file mode 100644 index 8cc0b464..00000000 --- a/lua_scenes/quantum_accretion.lua +++ /dev/null @@ -1,205 +0,0 @@ --- ============================================================================ --- File: quantum_accretion.lua --- Concept: Domain-Warped Fractal Brownian Motion (FBM) with Differential Rotation --- ============================================================================ - -name = "quantum_accretion" - --- ============================================================================ --- LUA PERFORMANCE OPTIMIZATION --- Caching global math functions to local variables speeds up execution by ~30% --- inside the deep nested render loop. --- ============================================================================ -local m_sin = math.sin -local m_cos = math.cos -local m_floor = math.floor -local m_abs = math.abs -local m_sqrt = math.sqrt -local m_max = math.max -local m_min = math.min -local m_atan2 = math.atan2 - --- ============================================================================ --- SETUP & INITIALIZATION --- ============================================================================ -function setup() - -- Visual controllers - define_property("speed", "float", 0.5, 0.0, 3.0) - define_property("zoom", "float", 2.5, 0.5, 10.0) - define_property("intensity", "float", 1.2, 0.1, 5.0) - - -- Int property: Controls the fractal octaves (higher = more detail, lower FPS) - define_property("detail_level", "int", 3, 1, 5) - - -- Colors: Core (Fiery Orange/White) and Nebula (Deep Space Blue/Purple) - define_property("core_color", "color", 0xFF8822) - define_property("nebula_color", "color", 0x220088) -end - -function initialize() - log("Quantum Accretion Initialized: Engaging FBM engine at " .. width .. "x" .. height) -end - --- ============================================================================ --- PROCEDURAL NOISE ENGINE (Pure Math) --- ============================================================================ - --- Fast fractional part -local function fract(x) - return x - m_floor(x) -end - --- 2D Hash function for pseudo-random gradient mapping -local function hash(x, y) - local a = x * 12.9898 + y * 78.233 - return fract(m_sin(a) * 43758.5453123) -end - --- Smooth 2D Value Noise -local function noise(x, y) - local ix, iy = m_floor(x), m_floor(y) - local fx, fy = fract(x), fract(y) - - -- Smoothstep interpolation - local ux = fx * fx * (3.0 - 2.0 * fx) - local uy = fy * fy * (3.0 - 2.0 * fy) - - local a = hash(ix, iy) - local b = hash(ix + 1.0, iy) - local c = hash(ix, iy + 1.0) - local d = hash(ix + 1.0, iy + 1.0) - - return a + (b - a)*ux + (c - a)*uy + (a - b - c + d)*ux*uy -end - --- "Ridge" Fractal Brownian Motion: Creates lightning-like plasma tendrils -local function ridge_fbm(x, y, octaves) - local v = 0.0 - local amplitude = 0.5 - local frequency = 1.0 - - -- Rotation matrix to break up grid artifacts - local cos2, sin2 = 0.866, 0.5 - - for i = 1, octaves do - -- Fold the noise to create sharp ridges - local n = m_abs(noise(x * frequency, y * frequency) * 2.0 - 1.0) - v = v + (1.0 - n) * amplitude - - -- Rotate and scale coordinates for next octave - local nx = x * cos2 - y * sin2 - local ny = x * sin2 + y * cos2 - x, y = nx * 2.0, ny * 2.0 - - amplitude = amplitude * 0.5 - end - return v -end - --- Smoothstep helper for soft edges/event horizons -local function smoothstep(edge0, edge1, x) - local t = m_max(0.0, m_min(1.0, (x - edge0) / (edge1 - edge0))) - return t * t * (3.0 - 2.0 * t) -end - --- ============================================================================ --- MAIN RENDER LOOP --- ============================================================================ -function render() - clear() - - -- 1. Extract and Cast Properties - local speed = get_property("speed") - local zoom = get_property("zoom") - local intensity = get_property("intensity") - local detail = m_floor(get_property("detail_level")) -- Coerce to int - - -- Unpack Core Color (24-bit Hex) - local hex_core = get_property("core_color") - local cr = m_floor(hex_core / 65536) % 256 - local cg = m_floor(hex_core / 256) % 256 - local cb = hex_core % 256 - - -- Unpack Nebula Color (24-bit Hex) - local hex_neb = get_property("nebula_color") - local nr = m_floor(hex_neb / 65536) % 256 - local ng = m_floor(hex_neb / 256) % 256 - local nb = hex_neb % 256 - - local t = time * speed - - -- Aspect ratio adjustment - local aspect = width / height - local half_w = width * 0.5 - local half_h = height * 0.5 - - -- 2. Pixel Iteration Loop - for px = 0, width - 1 do - for py = 0, height - 1 do - - -- Normalize UV coordinates (-1.0 to 1.0) - local uv_x = (px - half_w) / half_h * aspect - local uv_y = (py - half_h) / half_h - - -- Distance and angle from center - local dist = m_sqrt(uv_x*uv_x + uv_y*uv_y) - - -- Differential Rotation: Closer to center spins faster - local angle = t + (1.0 / (dist + 0.2)) - local s, c = m_sin(angle), m_cos(angle) - - -- Rotate and apply zoom - local rx = (uv_x * c - uv_y * s) * zoom - local ry = (uv_x * s + uv_y * c) * zoom - - -- Domain Warping (Distorting space with noise before calculating noise) - local warp_x = ridge_fbm(rx + t * 0.2, ry - t * 0.1, 2) - local warp_y = ridge_fbm(rx - t * 0.3, ry + t * 0.2, 2) - - -- Calculate the final Plasma density - local plasma = ridge_fbm(rx + warp_x * 2.0, ry + warp_y * 2.0, detail) - - -- Apply "Event Horizon" mask (dark in the very center, fading out) - local horizon = smoothstep(0.1, 0.5, dist) - plasma = plasma * horizon - - -- Core Glow (Inverse square falloff from center) - local glow = (0.03 * intensity) / (dist + 0.001) - - -- 3. Thermodynamic Color Compositing - -- We transition: Dark Space -> Nebula Gas -> Searing Core - local mix_factor = m_max(0.0, m_min(1.0, plasma * intensity)) - - local final_r, final_g, final_b = 0, 0, 0 - - if mix_factor < 0.5 then - -- Blend Dark to Nebula - local st = mix_factor * 2.0 - final_r = nr * st - final_g = ng * st - final_b = nb * st - else - -- Blend Nebula to Core - local st = (mix_factor - 0.5) * 2.0 - final_r = nr + (cr - nr) * st - final_g = ng + (cg - ng) * st - final_b = nb + (cb - nb) * st - end - - -- Add pure physical core glow on top (additive blending) - final_r = final_r + (cr * glow) - final_g = final_g + (cg * glow) - final_b = final_b + (cb * glow) - - -- Clamp safe output (0-255) - final_r = m_max(0, m_min(255, m_floor(final_r))) - final_g = m_max(0, m_min(255, m_floor(final_g))) - final_b = m_max(0, m_min(255, m_floor(final_b))) - - -- Draw to screen - set_pixel(px, py, final_r, final_g, final_b) - end - end - - return true -end \ No newline at end of file diff --git a/lua_scenes/rainbow_spiral.lua b/lua_scenes/rainbow_spiral.lua deleted file mode 100644 index 7df71900..00000000 --- a/lua_scenes/rainbow_spiral.lua +++ /dev/null @@ -1,89 +0,0 @@ --- lua_scenes/rainbow_spiral.lua --- A slowly-rotating spiral whose hue and speed can be tweaked at runtime. - -name = "rainbow_spiral" - -------------------------------------------------- --- 1. Declare tweakable properties -------------------------------------------------- -function setup() - define_property("speed", "float", 0.8, 0.1, 3.0) -- rotation speed - define_property("arms", "int", 3, 1, 8) -- number of spiral arms - define_property("tightness", "float", 3.0, 1.0, 8.0) - define_property("brightness", "float", 1.0, 0.2, 1.0) - define_property("bg_tint", "color", 0x000010) -- deep-blue background -end - -------------------------------------------------- --- 2. One-time setup when scene becomes visible -------------------------------------------------- -function initialize() - -- nothing special needed here -end - -------------------------------------------------- --- 3. Per-frame drawing -------------------------------------------------- -function render() - clear() -- start with black canvas - - -- unpack background tint - local raw_bg = get_property("bg_tint") - local bg_r = math.floor(raw_bg / 65536) % 256 - local bg_g = math.floor(raw_bg / 256) % 256 - local bg_b = raw_bg % 256 - -- fill canvas with tinted background - for y = 0, height - 1 do - for x = 0, width - 1 do - --- set_pixel(x, y, bg_r, bg_g, bg_b) - end - end - - -- read live properties - local speed = get_property("speed") - local arms = math.floor(get_property("arms")) - local tightness = get_property("tightness") - local brightness = get_property("brightness") - - local cx = width / 2 - local cy = height / 2 - local max_r = math.min(cx, cy) - - -- precompute angle offset that grows with time - local angle_shift = speed * time * 2.0 * math.pi - - for y = 0, height - 1 do - for x = 0, width - 1 do - local dx = x - cx - local dy = y - cy - local r = math.sqrt(dx*dx + dy*dy) - if r <= max_r then - local angle = math.atan(dy, dx) + angle_shift - local spiral = angle * arms - r / tightness - local hue = (spiral / (2 * math.pi)) % 1 - local sat = 1.0 - local val = brightness * (1.0 - r / max_r) -- fade at edges - - -- fast HSVβ†’RGB (simplified) - local h6 = hue * 6 - local c = val * sat - local x1 = c * (1 - math.abs((h6 % 2) - 1)) - local m = val - c - local r_, g_, b_ - if h6 < 1 then r_, g_, b_ = c, x1, 0 - elseif h6 < 2 then r_, g_, b_ = x1, c, 0 - elseif h6 < 3 then r_, g_, b_ = 0, c, x1 - elseif h6 < 4 then r_, g_, b_ = 0, x1, c - elseif h6 < 5 then r_, g_, b_ = x1, 0, c - else r_, g_, b_ = c, 0, x1 - end - local R = math.floor((r_ + m) * 255) - local G = math.floor((g_ + m) * 255) - local B = math.floor((b_ + m) * 255) - set_pixel(x, y, R, G, B) - end - end - end - - return true -- keep scene alive -end \ No newline at end of file diff --git a/lua_scenes/realistic_cloth.lua b/lua_scenes/realistic_cloth.lua deleted file mode 100644 index 50e514dd..00000000 --- a/lua_scenes/realistic_cloth.lua +++ /dev/null @@ -1,225 +0,0 @@ --- 1. Script Contract -name = "realistic_cloth" - --- Local state variables -local points = {} -local links = {} -local prev_dt = 0.016 -local cols = 14 -local rows = 14 - --- 2. Setup: Declare properties -function setup() - define_property("gravity", "float", 400.0, 50.0, 1000.0) - define_property("wind_strength", "float", 100.0, 0.0, 500.0) - define_property("stiffness", "int", 6, 1, 15) -- Number of constraint iterations - define_property("cloth_color", "color", 0x00FF88) -- Spring green - define_property("sphere_color", "color", 0xFF2255) -- Crimson - define_property("draw_sphere", "bool", true) -end - --- Helper: Bresenham's Line Algorithm to draw the cloth links -local function draw_line(x0, y0, x1, y1, r, g, b) - x0 = math.floor(x0 + 0.5) - y0 = math.floor(y0 + 0.5) - x1 = math.floor(x1 + 0.5) - y1 = math.floor(y1 + 0.5) - - local dx = math.abs(x1 - x0) - local dy = -math.abs(y1 - y0) - local sx = x0 < x1 and 1 or -1 - local sy = y0 < y1 and 1 or -1 - local err = dx + dy - - while true do - set_pixel(x0, y0, r, g, b) - if x0 == x1 and y0 == y1 then break end - local e2 = 2 * err - if e2 >= dy then - err = err + dy - x0 = x0 + sx - end - if e2 <= dx then - err = err + dx - y0 = y0 + sy - end - end -end - --- Helper: Draw a solid circle for the collision object -local function draw_solid_circle(cx, cy, radius, r, g, b) - for y = -radius, radius do - for x = -radius, radius do - if x*x + y*y <= radius*radius then - set_pixel(math.floor(cx + x), math.floor(cy + y), r, g, b) - end - end - end -end - --- 3. Initialize: Reset simulation state -function initialize() - points = {} - links = {} - prev_dt = 0.016 - - -- Calculate cloth dimensions relative to canvas - local spacing = math.floor(width / (cols + 4)) - local start_x = (width - (cols - 1) * spacing) / 2 - local start_y = 5 - - -- Generate particles - for y = 0, rows - 1 do - for x = 0, cols - 1 do - local px = start_x + x * spacing - local py = start_y + y * spacing - table.insert(points, { - x = px, y = py, - ox = px, oy = py, -- Old x, y for Verlet velocity - pinned = (y == 0) -- Pin the top row - }) - end - end - - -- Generate constraints (structural links) - local function get_idx(x, y) return y * cols + x + 1 end - - for y = 0, rows - 1 do - for x = 0, cols - 1 do - if x < cols - 1 then - table.insert(links, { p1 = get_idx(x, y), p2 = get_idx(x+1, y), rest = spacing }) - end - if y < rows - 1 then - table.insert(links, { p1 = get_idx(x, y), p2 = get_idx(x, y+1), rest = spacing }) - end - end - end - - log("Cloth physics initialized. Nodes: " .. tostring(#points) .. ", Links: " .. tostring(#links)) -end - --- 4. Render: Called every frame -function render() - clear() - - -- Property extraction - local gravity = get_property("gravity") - local wind_strength = get_property("wind_strength") - local stiffness = math.floor(get_property("stiffness")) - local raw_cloth = get_property("cloth_color") - local raw_sphere = get_property("sphere_color") - - -- Color unpacking - local cr = math.floor(raw_cloth / 65536) % 256 - local cg = math.floor(raw_cloth / 256) % 256 - local cb = raw_cloth % 256 - local sr = math.floor(raw_sphere / 65536) % 256 - local sg = math.floor(raw_sphere / 256) % 256 - local sb = raw_sphere % 256 - - -- Cap delta time to prevent physics explosions during lag spikes - local safe_dt = math.min(dt, 0.05) - if prev_dt == 0 then prev_dt = safe_dt end - local dt_ratio = safe_dt / prev_dt - - -- Dynamic variables - local wind_x = math.sin(time * 1.5) * math.cos(time * 0.8) * wind_strength - local wind_y = math.sin(time * 2.1) * (wind_strength * 0.2) - - -- Kinematic Sphere (Moves around in a figure-8) - local sphere_r = 20.0 - local sphere_x = (width / 2) + math.sin(time) * (width / 3) - local sphere_y = (height / 2) + math.sin(time * 2.0) * (height / 4) + 15 - - -- Render the sphere if requested - if get_property("draw_sphere") then - draw_solid_circle(sphere_x, sphere_y, sphere_r, sr, sg, sb) - end - - -- --- PHYSICS INTEGRATION (Time-Corrected Verlet) --- - for i, p in ipairs(points) do - if not p.pinned then - -- Calculate forces - local acc_x = wind_x - local acc_y = gravity + wind_y - - -- Velocity derived from previous frame (scaled by dt_ratio for framerate independence) - local vx = (p.x - p.ox) * dt_ratio * 0.99 -- 0.99 is air friction - local vy = (p.y - p.oy) * dt_ratio * 0.99 - - p.ox = p.x - p.oy = p.y - - -- Apply motion - p.x = p.x + vx + acc_x * (safe_dt * safe_dt) - p.y = p.y + vy + acc_y * (safe_dt * safe_dt) - - -- Sphere Collision Detection - local dx = p.x - sphere_x - local dy = p.y - sphere_y - local dist2 = dx*dx + dy*dy - - if dist2 < sphere_r * sphere_r then - local dist = math.sqrt(dist2) - local overlap = sphere_r - dist - - -- Push particle out of the sphere - local nx = dx / dist - local ny = dy / dist - p.x = p.x + nx * overlap - p.y = p.y + ny * overlap - - -- Friction against the sphere (dampen stored velocity) - p.ox = p.x - (p.x - p.ox) * 0.5 - end - - -- Floor / Wall constraints - if p.y > height - 1 then p.y = height - 1 end - if p.x < 0 then p.x = 0 end - if p.x > width - 1 then p.x = width - 1 end - end - end - - -- --- CONSTRAINTS RESOLUTION (Hooke's Law approximation) --- - -- Multiple iterations stiffen the cloth and distribute forces - for iter = 1, stiffness do - for i, link in ipairs(links) do - local p1 = points[link.p1] - local p2 = points[link.p2] - - local dx = p2.x - p1.x - local dy = p2.y - p1.y - local dist = math.sqrt(dx*dx + dy*dy) - - -- Resolve spring length difference - if dist > 0.0001 then - local difference = (dist - link.rest) / dist - local offset_x = dx * 0.5 * difference - local offset_y = dy * 0.5 * difference - - if not p1.pinned then - p1.x = p1.x + offset_x - p1.y = p1.y + offset_y - end - - if not p2.pinned then - p2.x = p2.x - offset_x - p2.y = p2.y - offset_y - end - end - end - end - - -- Update previous delta time for next frame's Verlet calculations - prev_dt = safe_dt - - -- --- RENDERING --- - -- Draw the cloth links using Bresenham's line algorithm - for i, link in ipairs(links) do - local p1 = points[link.p1] - local p2 = points[link.p2] - draw_line(p1.x, p1.y, p2.x, p2.y, cr, cg, cb) - end - - return true -end \ No newline at end of file diff --git a/lua_scenes/starfield_warp.lua b/lua_scenes/starfield_warp.lua deleted file mode 100644 index 83f3b319..00000000 --- a/lua_scenes/starfield_warp.lua +++ /dev/null @@ -1,100 +0,0 @@ --- 1. Script Contract: Required global name (unique, no spaces) -name = "starfield_warp" - --- Local state variables to hold our scene data -local stars = {} - --- 2. Setup: Called once. Strictly used for defining properties. -function setup() - -- define_property(name, type, default, min, max) - define_property("star_count", "int", 150, 10, 500) - define_property("speed", "float", 80.0, 10.0, 300.0) - define_property("warp_color", "color", 0x00FFFF) -- Cyan by default - define_property("wobble_camera", "bool", true) - define_property("clear_bg", "bool", true) -end - --- 3. Initialize: Called when scene is first displayed or re-initialized. -function initialize() - -- Reset state - stars = {} - - -- NOTE: get_property("int") returns a float, so we MUST wrap in math.floor() - local count = math.floor(get_property("star_count")) - - -- Populate initial stars - for i = 1, count do - table.insert(stars, { - x = (math.random() * width) - (width / 2), - y = (math.random() * height) - (height / 2), - z = math.random() * width -- Depth - }) - end - - log("Starfield scene initialized with " .. tostring(count) .. " stars.") -end - --- 4. Render: Called every frame. Must end with return true. -function render() - -- Clear canvas rapidly with black if property is true - if get_property("clear_bg") then - clear() - end - - -- Retrieve configured properties - local speed = get_property("speed") - local raw_color = get_property("warp_color") - local wobble = get_property("wobble_camera") - - -- 5. Critical Pitfall: Color Extraction - local r = math.floor(raw_color / 65536) % 256 - local g = math.floor(raw_color / 256) % 256 - local b = raw_color % 256 - - -- Calculate screen center (using `width` and `height` globals) - local cx = width / 2 - local cy = height / 2 - - -- Add dynamic movement using the `time` global - if wobble then - cx = cx + (math.sin(time) * (width / 8)) - cy = cy + (math.cos(time * 0.8) * (height / 8)) - end - - cx = math.floor(cx) - cy = math.floor(cy) - - -- Update and draw each star - for i, star in ipairs(stars) do - -- Move star closer to camera using `dt` (delta time) - star.z = star.z - (speed * dt) - - -- If star passes the camera (z <= 0), respawn it far away - if star.z <= 0 then - star.z = width - star.x = (math.random() * width) - (width / 2) - star.y = (math.random() * height) - (height / 2) - end - - -- Project 3D coordinates to 2D screen space - -- math.max prevents divide-by-zero crashes - local pz = math.max(star.z, 0.001) - local px = math.floor((star.x / pz) * width + cx) - local py = math.floor((star.y / pz) * width + cy) - - -- Fade stars in as they get closer (intensity 0.0 to 1.0) - local intensity = 1.0 - (star.z / width) - if intensity < 0 then intensity = 0 end - if intensity > 1 then intensity = 1 end - - local final_r = math.floor(r * intensity) - local final_g = math.floor(g * intensity) - local final_b = math.floor(b * intensity) - - -- Draw the pixel (Out-of-bounds coordinates are safely ignored by the API) - set_pixel(px, py, final_r, final_g, final_b) - end - - -- 6. Script Contract: MUST return true - return true -end \ No newline at end of file diff --git a/plugins/ScriptedScenes/CMakeLists.txt b/plugins/ScriptedScenes/CMakeLists.txt deleted file mode 100644 index 903e74bc..00000000 --- a/plugins/ScriptedScenes/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# ScriptedScenes – Lua scripted scene plugin (matrix and desktop) -# -# lua and sol2 are installed via vcpkg through the "scripted-scenes-matrix" -# and "scripted-scenes-desktop" features in vcpkg.json. - -find_package(Lua REQUIRED) -find_package(sol2 CONFIG REQUIRED) - -if(ENABLE_DESKTOP) - register_plugin(ScriptedScenes - DESKTOP - desktop/ScriptedScenesDesktop.cpp - desktop/ScriptedScenesDesktop.h - ) -else() - register_plugin(ScriptedScenes - matrix/ScriptedScenes.cpp - matrix/ScriptedScenes.h - matrix/LuaScene.cpp - matrix/LuaScene.h - matrix/LatestLuaScene.cpp - matrix/LatestLuaScene.h - ) -endif() - -# sol2::sol2 is header-only, so we must explicitly link the Lua library -target_link_libraries(ScriptedScenes PRIVATE sol2::sol2 ${LUA_LIBRARIES}) -target_include_directories(ScriptedScenes PRIVATE ${LUA_INCLUDE_DIR}) diff --git a/plugins/ScriptedScenes/desktop/ScriptedScenesDesktop.cpp b/plugins/ScriptedScenes/desktop/ScriptedScenesDesktop.cpp deleted file mode 100644 index a0d266a9..00000000 --- a/plugins/ScriptedScenes/desktop/ScriptedScenesDesktop.cpp +++ /dev/null @@ -1,1296 +0,0 @@ -#include "ScriptedScenesDesktop.h" - -#include -#include -#include - -extern "C" PLUGIN_EXPORT ScriptedScenesDesktop* createScriptedScenes() -{ - return new ScriptedScenesDesktop(); -} - -extern "C" PLUGIN_EXPORT void destroyScriptedScenes(ScriptedScenesDesktop* c) -{ - delete c; -} - -static double get_time_sec() -{ - auto now = std::chrono::steady_clock::now(); - return std::chrono::duration(now.time_since_epoch()).count(); -} - -static constexpr int MIN_RENDER_DOWNSCALE = 1; -static constexpr int MAX_RENDER_DOWNSCALE = 4; - -// Adaptive tuning constants -static constexpr int ADAPTIVE_SCALE_UP_BURST_THRESHOLD = 2; -static constexpr int ADAPTIVE_SCALE_DOWN_STABLE_SECONDS = 10; -static constexpr int ADAPTIVE_MIN_WORKERS = 1; -static constexpr int ADAPTIVE_LOOKAHEAD_STEP_UP = 2; -static constexpr int ADAPTIVE_LOOKAHEAD_MAX = 30; -static constexpr int ADAPTIVE_LOOKAHEAD_MIN = 4; -static constexpr int ADAPTIVE_QUEUE_STEP_UP = 8; -static constexpr int ADAPTIVE_QUEUE_MAX = 120; -static constexpr int ADAPTIVE_QUEUE_MIN = 16; -static constexpr float ADAPTIVE_FPS_POOR_THRESHOLD = 0.90f; -static constexpr float ADAPTIVE_FPS_GOOD_THRESHOLD = 0.98f; -static constexpr float ADAPTIVE_REORDER_STEP_UP_MS = 50.0f; -static constexpr float ADAPTIVE_REORDER_MAX_MS = 2000.0f; -static constexpr float ADAPTIVE_REORDER_STEP_DOWN_MS = 5.0f; -static constexpr float ADAPTIVE_REORDER_MIN_MS = 34.0f; - -ScriptedScenesDesktop::ScriptedScenesDesktop() -{ - canvas_data_.resize(matrix_width_ * matrix_height_ * 3, 0); - update_script_dimensions_locked(); -} - -ScriptedScenesDesktop::~ScriptedScenesDesktop() -{ - std::lock_guard lock(script_mutex_); - stop_pipeline_workers_locked(); -} - -void ScriptedScenesDesktop::initialize_imgui(ImGuiContext* im_gui_context, ImGuiMemAllocFunc* alloc_fn, - ImGuiMemFreeFunc* free_fn, void** user_data) -{ - ImGui::SetCurrentContext(im_gui_context); - ImGui::GetAllocatorFunctions(alloc_fn, free_fn, user_data); -} - -void ScriptedScenesDesktop::render() -{ - std::lock_guard lock(script_mutex_); - if (!is_lua_loaded_) - { - ImGui::Text("No Lua script loaded."); - return; - } - - ImGui::Text("Current Scene: %s", current_scene_name_.c_str()); - ImGui::Text("Offload Render: %s", offload_render_ ? "Yes" : "No"); - ImGui::Text("FPS: %.1f", current_fps_); - - bool should_restart_pipeline = false; - - if (ImGui::SliderInt("Render Downscale", &render_downscale_, MIN_RENDER_DOWNSCALE, MAX_RENDER_DOWNSCALE)) - { - update_script_dimensions_locked(); - if (lua_) - { - (*lua_)["width"] = script_width_; - (*lua_)["height"] = script_height_; - } - should_restart_pipeline = true; - } - - ImGui::Text("Lua Resolution: %dx%d", script_width_, script_height_); - - if (ImGui::Checkbox("Unsafe Parallel Mode", &bypass_protected_calls_)) - { - should_restart_pipeline = true; - } - - const bool manual_control = !adaptive_pipeline_; - - if (!manual_control) ImGui::BeginDisabled(); - - if (ImGui::SliderInt("Pipeline Workers", &pipeline_worker_count_, 1, MAX_WORKERS)) - { - sync_pipeline_workers_locked(); // Seamless adjustment - } - - ImGui::SliderInt("Pipeline Lookahead", &pipeline_lookahead_depth_, 1, 30); - ImGui::SliderInt("Pipeline Max Queue", &pipeline_max_queued_frames_, 4, 120); - ImGui::SliderFloat("Max Reorder Wait (ms)", &pipeline_max_reorder_wait_ms_, 0.0f, 2000.0f, "%.1f"); - - if (!manual_control) ImGui::EndDisabled(); - - ImGui::Text("Deterministic parallel: %s", deterministic_parallel_ ? "Yes" : "No"); - ImGui::Text("Parallel active: %s", use_parallel_pipeline_ ? "Yes" : "No"); - - if (should_restart_pipeline) - { - stop_pipeline_workers_locked(); - maybe_update_pipeline_mode_locked(); - } - - ImGui::Separator(); - ImGui::Text("Lua Profiling (1s avg)"); - ImGui::Text("Compute total: %.3f ms", profile_avg_total_ms_); - ImGui::Text("Lua render(): %.3f ms", profile_avg_lua_render_ms_); - ImGui::Text("Global updates: %.3f ms", profile_avg_globals_ms_); - ImGui::Text("Packet creation: %.3f ms", profile_avg_packet_ms_); - ImGui::Text("set_pixel/frame: %.1f", profile_avg_set_pixel_calls_per_frame_); - ImGui::Text("clear/frame: %.2f", profile_avg_clear_calls_per_frame_); - - if (use_parallel_pipeline_) - { - ImGui::Separator(); - ImGui::Text("Parallel Pipeline"); - ImGui::Text("Queue depth: %.0f", pipeline_queue_depth_); - ImGui::Text("Completed depth: %.0f", pipeline_completed_depth_); - ImGui::Text("Reorder wait: %.2f ms", pipeline_reorder_wait_ms_); - ImGui::Text("Dropped frames: %llu", static_cast(pipeline_frames_dropped_)); - ImGui::Text("Sent frames: %llu", static_cast(pipeline_frames_sent_)); - ImGui::Text("Effective send FPS: %.1f", pipeline_effective_send_fps_); - - const double base_frame_budget_ms = 1000.0 / static_cast(std::max(1.0f, pipeline_target_fps_)); - const double parallel_budget_ms = base_frame_budget_ms * pipeline_worker_count_ * 0.95; - - ImGui::Text("Worker render avg: %.3f ms", pipeline_avg_worker_render_ms_); - ImGui::Text("Worker total avg: %.3f ms", pipeline_avg_worker_total_ms_); - ImGui::Text("Parallel budget: %.2f ms", parallel_budget_ms); - } - - ImGui::Separator(); - ImGui::Text("Adaptive Pipeline Tuning"); - - if (ImGui::Checkbox("Enable Adaptive Mode", &adaptive_pipeline_)) - { - adaptive_last_drop_count_ = pipeline_frames_dropped_; - adaptive_stable_seconds_ = 0; - adaptive_drop_bursts_ = 0; - adaptive_drops_last_window_= 0; - adaptive_fps_ratio_last_ = 1.0f; - adaptive_last_action_ = "none"; - } - - if (adaptive_pipeline_) - { - ImGui::Text("FPS ratio (eff/target): %.2f", adaptive_fps_ratio_last_); - ImGui::Text("Drops last window: %llu", static_cast(adaptive_drops_last_window_)); - ImGui::Text("Stable seconds: %d", adaptive_stable_seconds_); - ImGui::Text("Drop burst count: %d", adaptive_drop_bursts_); - ImGui::Text("Last action: %s", adaptive_last_action_.c_str()); - } -} - -void ScriptedScenesDesktop::on_websocket_message(const std::string message) -{ - if (message.starts_with("size:")) - { - std::string sizeStr = message.substr(5); - auto xPos = sizeStr.find('x'); - if (xPos != std::string::npos) - { - std::lock_guard lock(script_mutex_); - matrix_width_ = std::stoi(sizeStr.substr(0, xPos)); - matrix_height_ = std::stoi(sizeStr.substr(xPos + 1)); - canvas_data_.resize(matrix_width_ * matrix_height_ * 3, 0); - update_script_dimensions_locked(); - - if (lua_) - { - (*lua_)["width"] = script_width_; - (*lua_)["height"] = script_height_; - } - - stop_pipeline_workers_locked(); - maybe_update_pipeline_mode_locked(); - } - } - else if (message.starts_with("script:")) - { - std::string payload = message.substr(7); - auto pos = payload.find(':'); - if (pos != std::string::npos) - { - std::string name = payload.substr(0, pos); - std::string script_content = payload.substr(pos + 1); - - spdlog::info("[ScriptedScenesDesktop] Received script for '{}'", name); - current_scene_name_ = name; - - std::lock_guard lock(script_mutex_); - default_properties_.clear(); - stop_pipeline_workers_locked(); - setup_lua_state(); - if (load_and_exec_script(script_content)) - { - sol::protected_function setup_fn = (*lua_)["setup"]; - if (setup_fn.valid()) - { - auto result = setup_fn(); - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[ScriptedScenesDesktop] setup() error: {}", err.what()); - } - } - - (*lua_)["width"] = script_width_; - (*lua_)["height"] = script_height_; - - sol::protected_function init_fn = (*lua_)["initialize"]; - if (init_fn.valid()) - { - auto result = init_fn(); - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[ScriptedScenesDesktop] initialize() error: {}", err.what()); - } - } - - reset_profiling_locked(get_time_sec()); - - std::fill(canvas_data_.begin(), canvas_data_.end(), 0); - std::fill(script_canvas_data_.begin(), script_canvas_data_.end(), 0); - - maybe_update_pipeline_mode_locked(); - } - } - } -} - -void ScriptedScenesDesktop::update_script_dimensions_locked() -{ - render_downscale_ = std::clamp(render_downscale_, MIN_RENDER_DOWNSCALE, MAX_RENDER_DOWNSCALE); - script_width_ = std::max(1, matrix_width_ / render_downscale_); - script_height_ = std::max(1, matrix_height_ / render_downscale_); - script_canvas_data_.assign(script_width_ * script_height_ * 3, 0); - - upscale_x_map_.resize(matrix_width_); - for (int x = 0; x < matrix_width_; ++x) - { - upscale_x_map_[x] = std::min(script_width_ - 1, x / render_downscale_); - } - - upscale_y_map_.resize(matrix_height_); - for (int y = 0; y < matrix_height_; ++y) - { - upscale_y_map_[y] = std::min(script_height_ - 1, y / render_downscale_); - } - - pipeline_render_config_.matrix_width = matrix_width_; - pipeline_render_config_.matrix_height = matrix_height_; - pipeline_render_config_.script_width = script_width_; - pipeline_render_config_.script_height = script_height_; - pipeline_render_config_.upscale_x_map = upscale_x_map_; - pipeline_render_config_.upscale_y_map = upscale_y_map_; -} - -void ScriptedScenesDesktop::blit_script_canvas_to_output(const std::vector& script_canvas, - std::vector& output_canvas, - const RenderConfig& config) -{ - output_canvas.resize(config.matrix_width * config.matrix_height * 3); - - if (config.script_width == config.matrix_width && config.script_height == config.matrix_height) - { - std::copy(script_canvas.begin(), script_canvas.end(), output_canvas.begin()); - return; - } - - for (int y = 0; y < config.matrix_height; ++y) - { - const int src_y = config.upscale_y_map[y]; - for (int x = 0; x < config.matrix_width; ++x) - { - const int src_x = config.upscale_x_map[x]; - const int src_idx = (src_y * config.script_width + src_x) * 3; - const int dst_idx = (y * config.matrix_width + x) * 3; - output_canvas[dst_idx] = script_canvas[src_idx]; - output_canvas[dst_idx + 1] = script_canvas[src_idx + 1]; - output_canvas[dst_idx + 2] = script_canvas[src_idx + 2]; - } - } -} - -void ScriptedScenesDesktop::blit_script_canvas_to_output_locked() -{ - blit_script_canvas_to_output(script_canvas_data_, canvas_data_, pipeline_render_config_); -} - -void ScriptedScenesDesktop::setup_lua_state() -{ - lua_ = std::make_unique(); - lua_->open_libraries( - sol::lib::base, - sol::lib::math, - sol::lib::string, - sol::lib::table - ); - - lua_->set_function("set_pixel", [this](int x, int y, int r, int g, int b) - { - profile_set_pixel_calls_++; - if (x < 0 || x >= script_width_ || y < 0 || y >= script_height_) return; - int idx = (y * script_width_ + x) * 3; - script_canvas_data_[idx] = static_cast(std::clamp(r, 0, 255)); - script_canvas_data_[idx + 1] = static_cast(std::clamp(g, 0, 255)); - script_canvas_data_[idx + 2] = static_cast(std::clamp(b, 0, 255)); - }); - - lua_->set_function("clear", [this]() - { - profile_clear_calls_++; - std::fill(script_canvas_data_.begin(), script_canvas_data_.end(), 0); - }); - - lua_->set_function("log", [this](const std::string& msg) - { - spdlog::info("[ScriptedScenesDesktop:{}] {}", current_scene_name_, msg); - }); - - lua_->set_function("define_property",[this](const std::string& name, const std::string& type, sol::object default_val, - sol::variadic_args va) - { - default_properties_[name] = default_val; - }); - - lua_->set_function("get_property", [this](const std::string& name) -> sol::object - { - auto it = default_properties_.find(name); - if (it != default_properties_.end()) - { - return it->second; - } - return sol::nil; - }); - - (*lua_)["width"] = script_width_; - (*lua_)["height"] = script_height_; - (*lua_)["time"] = 0.0; - (*lua_)["dt"] = 0.0; -} - -bool ScriptedScenesDesktop::load_and_exec_script(const std::string& script_content) -{ - script_content_ = script_content; - - auto result = lua_->script( - script_content,[](lua_State*, sol::protected_function_result pfr) { return pfr; } - ); - - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[ScriptedScenesDesktop:{}] Script load error: {}", current_scene_name_, err.what()); - is_lua_loaded_ = false; - return false; - } - - sol::object offload_obj = (*lua_)["offload"]; - if (offload_obj.is()) - { - offload_render_ = offload_obj.as(); - } - else - { - offload_render_ = true; - } - - render_downscale_ = 1; - sol::object render_downscale_obj = (*lua_)["render_downscale"]; - if (render_downscale_obj.is()) - { - render_downscale_ = render_downscale_obj.as(); - } - else if (render_downscale_obj.is()) - { - render_downscale_ = static_cast(render_downscale_obj.as()); - } - render_downscale_ = std::clamp(render_downscale_, MIN_RENDER_DOWNSCALE, MAX_RENDER_DOWNSCALE); - update_script_dimensions_locked(); - (*lua_)["width"] = script_width_; - (*lua_)["height"] = script_height_; - - sol::object deterministic_obj = (*lua_)["parallel_deterministic"]; - deterministic_parallel_ = !deterministic_obj.is() || deterministic_obj.as(); - - is_lua_loaded_ = true; - return true; -} - -void ScriptedScenesDesktop::reset_profiling_locked(double now) -{ - start_time_ = now; - last_time_ = now; - last_fps_update_ = now; - frame_count_ = 0; - current_fps_ = 0.0f; - profile_window_start_ = now; - profile_frames_ = 0; - profile_set_pixel_calls_ = 0; - profile_clear_calls_ = 0; - profile_globals_ms_sum_ = 0.0; - profile_lua_render_ms_sum_ = 0.0; - profile_packet_ms_sum_ = 0.0; - profile_total_ms_sum_ = 0.0; - profile_avg_globals_ms_ = 0.0f; - profile_avg_lua_render_ms_ = 0.0f; - profile_avg_packet_ms_ = 0.0f; - profile_avg_total_ms_ = 0.0f; - profile_avg_set_pixel_calls_per_frame_ = 0.0f; - profile_avg_clear_calls_per_frame_ = 0.0f; - - pipeline_frames_sent_ = 0; - pipeline_frames_dropped_ = 0; - pipeline_queue_depth_ = 0.0f; - pipeline_completed_depth_ = 0.0f; - pipeline_reorder_wait_ms_ = 0.0f; - pipeline_effective_send_fps_ = 0.0f; - pipeline_avg_worker_render_ms_ = 0.0f; - pipeline_avg_worker_total_ms_ = 0.0f; - pipeline_worker_render_ms_sum_ = 0.0; - pipeline_worker_total_ms_sum_ = 0.0; - pipeline_worker_samples_ = 0; - - adaptive_last_drop_count_ = 0; - adaptive_stable_seconds_ = 0; - adaptive_drop_bursts_ = 0; - adaptive_drops_last_window_ = 0; - adaptive_fps_ratio_last_ = 1.0f; - adaptive_last_action_ = "none"; -} - -void ScriptedScenesDesktop::stop_pipeline_workers_locked() -{ - { - std::lock_guard pipeline_lock(pipeline_mutex_); - stop_workers_ = true; - frame_jobs_.clear(); - } - pipeline_cv_.notify_all(); - - for (auto& worker : workers_) - { - if (worker->thread.joinable()) - { - worker->thread.join(); - } - } - - workers_.clear(); - workers_started_ = false; - use_parallel_pipeline_ = false; - - { - std::lock_guard pipeline_lock(pipeline_mutex_); - frame_jobs_.clear(); - completed_frames_.clear(); - stop_workers_ = false; - pipeline_queue_depth_ = 0.0f; - pipeline_completed_depth_ = 0.0f; - } -} - -bool ScriptedScenesDesktop::init_worker(WorkerState* worker_ctx) -{ - worker_ctx->render_config = pipeline_render_config_; - worker_ctx->scene_name = current_scene_name_; - worker_ctx->unsafe_mode = bypass_protected_calls_; - worker_ctx->lua = std::make_unique(); - worker_ctx->lua->open_libraries(sol::lib::base, sol::lib::math, sol::lib::string, sol::lib::table); - worker_ctx->script_canvas_data.assign(worker_ctx->render_config.script_width * worker_ctx->render_config.script_height * 3, 0); - worker_ctx->default_properties.clear(); - - const int canvas_width = worker_ctx->render_config.script_width; - const int canvas_height = worker_ctx->render_config.script_height; - - worker_ctx->lua->set_function("set_pixel",[worker_ctx, canvas_width, canvas_height](int x, int y, int r, int g, int b) - { - worker_ctx->set_pixel_calls++; - if (x < 0 || x >= canvas_width || y < 0 || y >= canvas_height) - return; - int idx = (y * canvas_width + x) * 3; - worker_ctx->script_canvas_data[idx] = static_cast(std::clamp(r, 0, 255)); - worker_ctx->script_canvas_data[idx + 1] = static_cast(std::clamp(g, 0, 255)); - worker_ctx->script_canvas_data[idx + 2] = static_cast(std::clamp(b, 0, 255)); - }); - - worker_ctx->lua->set_function("clear", [worker_ctx]() - { - worker_ctx->clear_calls++; - std::fill(worker_ctx->script_canvas_data.begin(), worker_ctx->script_canvas_data.end(), 0); - }); - - worker_ctx->lua->set_function("log",[worker_ctx](const std::string& msg) - { - spdlog::info("[ScriptedScenesDesktop:{}] Worker: {}", worker_ctx->scene_name, msg); - }); - - worker_ctx->lua->set_function("define_property", - [worker_ctx](const std::string& name, const std::string& type, sol::object default_val, - sol::variadic_args va) - { - worker_ctx->default_properties[name] = default_val; - }); - - worker_ctx->lua->set_function("get_property", [worker_ctx](const std::string& name) -> sol::object - { - auto it = worker_ctx->default_properties.find(name); - if (it != worker_ctx->default_properties.end()) - { - return it->second; - } - return sol::nil; - }); - - (*worker_ctx->lua)["width"] = canvas_width; - (*worker_ctx->lua)["height"] = canvas_height; - (*worker_ctx->lua)["time"] = 0.0; - (*worker_ctx->lua)["dt"] = 0.0; - - bool load_ok = true; - try - { - if (worker_ctx->unsafe_mode) - { - worker_ctx->lua->script(script_content_); - } - else - { - auto result = worker_ctx->lua->script( - script_content_,[](lua_State*, sol::protected_function_result pfr) { return pfr; }); - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[ScriptedScenesDesktop:{}] Worker script load error: {}", worker_ctx->scene_name, err.what()); - load_ok = false; - } - } - } - catch (const std::exception& ex) - { - spdlog::error("[ScriptedScenesDesktop:{}] Worker script load exception: {}", worker_ctx->scene_name, ex.what()); - load_ok = false; - } - - if (!load_ok) - { - worker_ctx->lua_loaded = false; - return false; - } - - try - { - if (worker_ctx->unsafe_mode) - { - sol::function setup_fn = (*worker_ctx->lua)["setup"]; - if (setup_fn.valid()) - { - setup_fn(); - } - - sol::function init_fn = (*worker_ctx->lua)["initialize"]; - if (init_fn.valid()) - { - init_fn(); - } - } - else - { - sol::protected_function setup_fn = (*worker_ctx->lua)["setup"]; - if (setup_fn.valid()) - { - auto setup_result = setup_fn(); - if (!setup_result.valid()) - { - sol::error err = setup_result; - spdlog::error("[ScriptedScenesDesktop:{}] Worker setup() error: {}", worker_ctx->scene_name, err.what()); - } - } - - sol::protected_function init_fn = (*worker_ctx->lua)["initialize"]; - if (init_fn.valid()) - { - auto init_result = init_fn(); - if (!init_result.valid()) - { - sol::error err = init_result; - spdlog::error("[ScriptedScenesDesktop:{}] Worker initialize() error: {}", worker_ctx->scene_name, err.what()); - } - } - } - } - catch (const std::exception& ex) - { - spdlog::error("[ScriptedScenesDesktop:{}] Worker init exception: {}", worker_ctx->scene_name, ex.what()); - worker_ctx->lua_loaded = false; - return false; - } - - worker_ctx->lua_loaded = true; - return true; -} - -void ScriptedScenesDesktop::sync_pipeline_workers_locked() -{ - // 1. Clean up officially finished workers - for (auto it = workers_.begin(); it != workers_.end(); ) - { - if ((*it)->is_finished) - { - if ((*it)->thread.joinable()) - { - (*it)->thread.join(); - } - it = workers_.erase(it); - } - else - { - ++it; - } - } - - // 2. Count actively running workers - int active_count = 0; - for (const auto& w : workers_) - { - if (!w->stop_requested) - { - active_count++; - } - } - - // 3. Scale up dynamically - while (active_count < pipeline_worker_count_) - { - auto new_worker = std::make_unique(); - new_worker->id = static_cast(workers_.size()); - - if (!init_worker(new_worker.get())) - { - break; // initialization failed - } - - WorkerState* raw_ptr = new_worker.get(); - workers_.push_back(std::move(new_worker)); - raw_ptr->thread = std::thread(&ScriptedScenesDesktop::worker_loop, this, raw_ptr); - active_count++; - } - - // 4. Scale down gracefully - if (active_count > pipeline_worker_count_) - { - std::lock_guard pipeline_lock(pipeline_mutex_); - for (auto it = workers_.rbegin(); it != workers_.rend(); ++it) - { - if (!(*it)->stop_requested) - { - (*it)->stop_requested = true; - active_count--; - if (active_count == pipeline_worker_count_) - { - break; - } - } - } - pipeline_cv_.notify_all(); - } -} - -bool ScriptedScenesDesktop::start_pipeline_workers_locked() -{ - stop_pipeline_workers_locked(); - - if (!is_lua_loaded_ || !offload_render_ || !deterministic_parallel_) - { - return false; - } - - { - std::lock_guard pipeline_lock(pipeline_mutex_); - completed_frames_.clear(); - frame_jobs_.clear(); - stop_workers_ = false; - } - - next_schedule_frame_index_ = 0; - next_send_frame_index_ = 0; - missing_frame_since_ = 0.0; - pipeline_start_time_ = get_time_sec(); - - sync_pipeline_workers_locked(); - - if (workers_.empty()) - { - stop_pipeline_workers_locked(); - return false; - } - - workers_started_ = true; - use_parallel_pipeline_ = true; - schedule_pipeline_jobs_locked(); - - spdlog::info( - "[ScriptedScenesDesktop:{}] Parallel pipeline started: workers={} lookahead={} max_queue={} unsafe_mode={}.", - current_scene_name_, pipeline_worker_count_, pipeline_lookahead_depth_, pipeline_max_queued_frames_, bypass_protected_calls_ ? "on" : "off"); - - return true; -} - -void ScriptedScenesDesktop::maybe_update_pipeline_mode_locked() -{ - const bool desired = is_lua_loaded_ && offload_render_ && deterministic_parallel_; - - if (!desired) - { - if (workers_started_) - stop_pipeline_workers_locked(); - return; - } - - if (!workers_started_) - { - start_pipeline_workers_locked(); - } -} - -void ScriptedScenesDesktop::schedule_pipeline_jobs_locked() -{ - if (!use_parallel_pipeline_) - return; - - const double dt = 1.0 / static_cast(std::max(1.0f, pipeline_target_fps_)); - - std::lock_guard pipeline_lock(pipeline_mutex_); - const uint64_t max_lookahead = static_cast(std::max(1, pipeline_lookahead_depth_)); - const size_t max_queue = static_cast(std::max(1, pipeline_max_queued_frames_)); - while (true) - { - const uint64_t outstanding_frames = - (next_schedule_frame_index_ > next_send_frame_index_) - ? (next_schedule_frame_index_ - next_send_frame_index_) - : 0; - const bool within_lookahead = outstanding_frames < max_lookahead; - const bool queue_has_capacity = (frame_jobs_.size() + completed_frames_.size()) < max_queue; - if (!within_lookahead || !queue_has_capacity) - break; - - const uint64_t frame_index = next_schedule_frame_index_; - frame_jobs_.push_back(FrameJob{frame_index, static_cast(frame_index) * dt, dt}); - next_schedule_frame_index_++; - } - - pipeline_queue_depth_ = static_cast(frame_jobs_.size()); - pipeline_completed_depth_ = static_cast(completed_frames_.size()); - pipeline_cv_.notify_all(); -} - -std::optional ScriptedScenesDesktop::try_take_next_pipeline_frame_locked(double now) -{ - std::lock_guard pipeline_lock(pipeline_mutex_); - - auto stale_end = completed_frames_.lower_bound(next_send_frame_index_); - completed_frames_.erase(completed_frames_.begin(), stale_end); - - auto exact = completed_frames_.find(next_send_frame_index_); - if (exact != completed_frames_.end()) - { - FrameResult result = std::move(exact->second); - completed_frames_.erase(exact); - missing_frame_since_ = 0.0; - pipeline_reorder_wait_ms_ = 0.0f; - pipeline_queue_depth_ = static_cast(frame_jobs_.size()); - pipeline_completed_depth_ = static_cast(completed_frames_.size()); - return result; - } - - if (missing_frame_since_ <= 0.0) - missing_frame_since_ = now; - - const float waited_ms = static_cast((now - missing_frame_since_) * 1000.0); - pipeline_reorder_wait_ms_ = waited_ms; - - if (waited_ms >= pipeline_max_reorder_wait_ms_) - { - if (!completed_frames_.empty()) - { - const uint64_t first_ready = completed_frames_.begin()->first; - if (first_ready > next_send_frame_index_) - { - pipeline_frames_dropped_ += (first_ready - next_send_frame_index_); - next_send_frame_index_ = first_ready; - } - - auto fallback = completed_frames_.find(next_send_frame_index_); - if (fallback != completed_frames_.end()) - { - FrameResult result = std::move(fallback->second); - completed_frames_.erase(fallback); - missing_frame_since_ = 0.0; - pipeline_reorder_wait_ms_ = 0.0f; - pipeline_queue_depth_ = static_cast(frame_jobs_.size()); - pipeline_completed_depth_ = static_cast(completed_frames_.size()); - return result; - } - } - - ++pipeline_frames_dropped_; - if (next_send_frame_index_ < next_schedule_frame_index_) - { - ++next_send_frame_index_; - } - missing_frame_since_ = now; - } - - pipeline_queue_depth_ = static_cast(frame_jobs_.size()); - pipeline_completed_depth_ = static_cast(completed_frames_.size()); - return std::nullopt; -} - -void ScriptedScenesDesktop::maybe_adapt_pipeline_locked() -{ - if (!adaptive_pipeline_ || !use_parallel_pipeline_) - return; - - const uint64_t drops_this_window = pipeline_frames_dropped_ - adaptive_last_drop_count_; - adaptive_last_drop_count_ = pipeline_frames_dropped_; - adaptive_drops_last_window_ = drops_this_window; - - const float fps_ratio = (pipeline_target_fps_ > 0.0f) - ? pipeline_effective_send_fps_ / pipeline_target_fps_ - : 1.0f; - adaptive_fps_ratio_last_ = fps_ratio; - - const double base_frame_budget_ms = 1000.0 / static_cast(std::max(1.0f, pipeline_target_fps_)); - const double parallel_budget_ms = base_frame_budget_ms * pipeline_worker_count_ * 0.95; - - const bool render_over_budget = pipeline_avg_worker_render_ms_ > static_cast(parallel_budget_ms); - const bool performing_poorly = - (drops_this_window > 0) || - (fps_ratio < ADAPTIVE_FPS_POOR_THRESHOLD) || - render_over_budget; - - if (performing_poorly) - { - adaptive_stable_seconds_ = 0; - ++adaptive_drop_bursts_; - - bool soft_changed = false; - - if (pipeline_lookahead_depth_ < ADAPTIVE_LOOKAHEAD_MAX) - { - pipeline_lookahead_depth_ = std::min( - pipeline_lookahead_depth_ + ADAPTIVE_LOOKAHEAD_STEP_UP, - ADAPTIVE_LOOKAHEAD_MAX); - soft_changed = true; - } - - if (pipeline_max_queued_frames_ < ADAPTIVE_QUEUE_MAX) - { - pipeline_max_queued_frames_ = std::min( - pipeline_max_queued_frames_ + ADAPTIVE_QUEUE_STEP_UP, - ADAPTIVE_QUEUE_MAX); - soft_changed = true; - } - - if (pipeline_max_reorder_wait_ms_ < ADAPTIVE_REORDER_MAX_MS) - { - pipeline_max_reorder_wait_ms_ = std::min( - pipeline_max_reorder_wait_ms_ + ADAPTIVE_REORDER_STEP_UP_MS, - ADAPTIVE_REORDER_MAX_MS); - soft_changed = true; - } - - if (soft_changed) - { - adaptive_last_action_ = "soft-up: lookahead=" + std::to_string(pipeline_lookahead_depth_) - + " queue=" + std::to_string(pipeline_max_queued_frames_); - - spdlog::info( - "[ScriptedScenesDesktop:{}] Adaptive soft-up: lookahead={} queue={} reorder_wait={:.1f}ms" - " (drops={} fps_ratio={:.2f} burst={})", - current_scene_name_, pipeline_lookahead_depth_, pipeline_max_queued_frames_, - pipeline_max_reorder_wait_ms_, drops_this_window, fps_ratio, adaptive_drop_bursts_); - } - - if (adaptive_drop_bursts_ >= ADAPTIVE_SCALE_UP_BURST_THRESHOLD && - pipeline_worker_count_ < MAX_WORKERS) - { - ++pipeline_worker_count_; - adaptive_last_action_ = "scale-up workers=" + std::to_string(pipeline_worker_count_); - - spdlog::info( - "[ScriptedScenesDesktop:{}] Adaptive scale-up: workers {} -> {} " - "(drops={} fps_ratio={:.2f} render_ms={:.2f} budget_ms={:.2f})", - current_scene_name_, pipeline_worker_count_ - 1, pipeline_worker_count_, - drops_this_window, fps_ratio, pipeline_avg_worker_render_ms_, parallel_budget_ms); - - adaptive_drop_bursts_ = 0; - // Removed complete restart. The worker sync handles adding threads transparently. - } - } - else - { - adaptive_drop_bursts_ = 0; - ++adaptive_stable_seconds_; - - if (adaptive_stable_seconds_ >= ADAPTIVE_SCALE_DOWN_STABLE_SECONDS) - { - const double simulated_lower_budget_ms = base_frame_budget_ms * (pipeline_worker_count_ - 1) * 0.90; - - if (pipeline_worker_count_ > ADAPTIVE_MIN_WORKERS && - pipeline_avg_worker_render_ms_ < static_cast(simulated_lower_budget_ms)) - { - --pipeline_worker_count_; - adaptive_last_action_ = "scale-down workers=" + std::to_string(pipeline_worker_count_); - - spdlog::info( - "[ScriptedScenesDesktop:{}] Adaptive scale-down: workers {} -> {} " - "(stable for {}s, fps_ratio={:.2f})", - current_scene_name_, pipeline_worker_count_ + 1, pipeline_worker_count_, - adaptive_stable_seconds_, fps_ratio); - - adaptive_stable_seconds_ = 0; - // Graceful retirement - } - else - { - bool soft_changed = false; - - if (pipeline_lookahead_depth_ > ADAPTIVE_LOOKAHEAD_MIN) - { - --pipeline_lookahead_depth_; - soft_changed = true; - } - - if (pipeline_max_queued_frames_ > ADAPTIVE_QUEUE_MIN) - { - pipeline_max_queued_frames_ = std::max( - pipeline_max_queued_frames_ - ADAPTIVE_QUEUE_STEP_UP / 2, - ADAPTIVE_QUEUE_MIN); - soft_changed = true; - } - - if (pipeline_max_reorder_wait_ms_ > ADAPTIVE_REORDER_MIN_MS) - { - pipeline_max_reorder_wait_ms_ = std::max( - pipeline_max_reorder_wait_ms_ - ADAPTIVE_REORDER_STEP_DOWN_MS, - ADAPTIVE_REORDER_MIN_MS); - soft_changed = true; - } - - if (soft_changed) - { - adaptive_last_action_ = "soft-down: lookahead=" + std::to_string(pipeline_lookahead_depth_) - + " queue=" + std::to_string(pipeline_max_queued_frames_); - - spdlog::info( - "[ScriptedScenesDesktop:{}] Adaptive soft-down: lookahead={} queue={} reorder_wait={:.1f}ms" - " (stable for {}s, fps_ratio={:.2f})", - current_scene_name_, pipeline_lookahead_depth_, pipeline_max_queued_frames_, - pipeline_max_reorder_wait_ms_, adaptive_stable_seconds_, fps_ratio); - } - - adaptive_stable_seconds_ = ADAPTIVE_SCALE_DOWN_STABLE_SECONDS / 2; - } - } - } -} - -void ScriptedScenesDesktop::worker_loop(WorkerState* worker) -{ - if (!worker) - { - return; - } - - while (true) - { - FrameJob job; - { - std::unique_lock pipeline_lock(pipeline_mutex_); - pipeline_cv_.wait(pipeline_lock, [this, worker]() - { - return stop_workers_ || worker->stop_requested || !frame_jobs_.empty(); - }); - - if (stop_workers_ || worker->stop_requested) - { - break; - } - - job = frame_jobs_.front(); - frame_jobs_.pop_front(); - pipeline_queue_depth_ = static_cast(frame_jobs_.size()); - } - - if (!worker->lua || !worker->lua_loaded) - { - continue; - } - - worker->set_pixel_calls = 0; - worker->clear_calls = 0; - - const double total_start = get_time_sec(); - (*worker->lua)["time"] = job.t; - (*worker->lua)["dt"] = job.dt; - - bool render_ok = true; - double render_ms = 0.0; - - try - { - if (worker->unsafe_mode) - { - const double render_start = get_time_sec(); - sol::function render_fn = (*worker->lua)["render"]; - if (render_fn.valid()) - { - render_fn(); - } - const double render_end = get_time_sec(); - render_ms = (render_end - render_start) * 1000.0; - } - else - { - const double render_start = get_time_sec(); - sol::protected_function render_fn = (*worker->lua)["render"]; - if (render_fn.valid()) - { - auto result = render_fn(); - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[ScriptedScenesDesktop:{}] worker render() error: {}", worker->scene_name, err.what()); - render_ok = false; - } - } - const double render_end = get_time_sec(); - render_ms = (render_end - render_start) * 1000.0; - } - } - catch (const std::exception& ex) - { - spdlog::error("[ScriptedScenesDesktop:{}] worker render() exception: {}", worker->scene_name, ex.what()); - render_ok = false; - } - - if (!render_ok) - { - continue; - } - - FrameResult result; - result.frame_index = job.frame_index; - result.set_pixel_calls = worker->set_pixel_calls; - result.clear_calls = worker->clear_calls; - result.render_ms = render_ms; - - blit_script_canvas_to_output(worker->script_canvas_data, result.frame_data, worker->render_config); - - const double total_end = get_time_sec(); - result.total_ms = (total_end - total_start) * 1000.0; - - { - std::lock_guard pipeline_lock(pipeline_mutex_); - if (!stop_workers_) - { - completed_frames_[result.frame_index] = std::move(result); - pipeline_completed_depth_ = static_cast(completed_frames_.size()); - } - } - } - - worker->is_finished = true; -} - -std::optional> ScriptedScenesDesktop::compute_next_packet( - const std::string sceneName) -{ - const double total_start = get_time_sec(); - std::lock_guard lock(script_mutex_); - - if ((sceneName != current_scene_name_ && sceneName != "Latest Lua Scene") || !is_lua_loaded_ || !offload_render_) - { - return std::nullopt; - } - - maybe_update_pipeline_mode_locked(); - - double current_time = get_time_sec(); - - if (use_parallel_pipeline_) - { - schedule_pipeline_jobs_locked(); - - auto next_frame = try_take_next_pipeline_frame_locked(current_time); - - std::optional> packet_opt = std::nullopt; - - if (profile_window_start_ <= 0.0) - profile_window_start_ = current_time; - - if (next_frame.has_value()) - { - const double packet_start = get_time_sec(); - auto packet = std::unique_ptr( - new ScriptedScenesPacket(next_frame->frame_data),[](UdpPacket* p) { delete dynamic_cast(p); } - ); - const double packet_end = get_time_sec(); - - ++next_send_frame_index_; - ++pipeline_frames_sent_; - - profile_frames_++; - profile_set_pixel_calls_ += next_frame->set_pixel_calls; - profile_clear_calls_ += next_frame->clear_calls; - profile_globals_ms_sum_ += 0.0; - profile_lua_render_ms_sum_ += next_frame->render_ms; - profile_packet_ms_sum_ += (packet_end - packet_start) * 1000.0; - profile_total_ms_sum_ += next_frame->total_ms; - - pipeline_worker_render_ms_sum_ += next_frame->render_ms; - pipeline_worker_total_ms_sum_ += next_frame->total_ms; - pipeline_worker_samples_++; - - packet_opt = std::move(packet); - } - - if (current_time - profile_window_start_ >= 1.0) - { - double elapsed = current_time - profile_window_start_; - pipeline_effective_send_fps_ = static_cast(profile_frames_) / static_cast(elapsed); - current_fps_ = pipeline_effective_send_fps_; - - if (profile_frames_ > 0) - { - const float frames = static_cast(profile_frames_); - profile_avg_globals_ms_ = static_cast(profile_globals_ms_sum_ / frames); - profile_avg_lua_render_ms_ = static_cast(profile_lua_render_ms_sum_ / frames); - profile_avg_packet_ms_ = static_cast(profile_packet_ms_sum_ / frames); - profile_avg_total_ms_ = static_cast(profile_total_ms_sum_ / frames); - profile_avg_set_pixel_calls_per_frame_ = static_cast(profile_set_pixel_calls_) / frames; - profile_avg_clear_calls_per_frame_ = static_cast(profile_clear_calls_) / frames; - } - else - { - profile_avg_globals_ms_ = 0.0f; - profile_avg_lua_render_ms_ = 0.0f; - profile_avg_packet_ms_ = 0.0f; - profile_avg_total_ms_ = 0.0f; - profile_avg_set_pixel_calls_per_frame_ = 0.0f; - profile_avg_clear_calls_per_frame_ = 0.0f; - } - - if (pipeline_worker_samples_ > 0) - { - pipeline_avg_worker_render_ms_ = static_cast(pipeline_worker_render_ms_sum_ / static_cast(pipeline_worker_samples_)); - pipeline_avg_worker_total_ms_ = static_cast(pipeline_worker_total_ms_sum_ / static_cast(pipeline_worker_samples_)); - } - - spdlog::info( - "[ScriptedScenesDesktop:{}] parallel avg: total={:.3f}ms render={:.3f}ms packet={:.3f}ms queue={:.0f} ready={:.0f} dropped={} fps={:.1f}", - current_scene_name_, profile_avg_total_ms_, profile_avg_lua_render_ms_, - profile_avg_packet_ms_, pipeline_queue_depth_, pipeline_completed_depth_, - pipeline_frames_dropped_, pipeline_effective_send_fps_); - - maybe_adapt_pipeline_locked(); - sync_pipeline_workers_locked(); // Seamlessly enforce changes and reap retired workers - - profile_window_start_ = current_time; - profile_frames_ = 0; - profile_set_pixel_calls_ = 0; - profile_clear_calls_ = 0; - profile_globals_ms_sum_ = 0.0; - profile_lua_render_ms_sum_ = 0.0; - profile_packet_ms_sum_ = 0.0; - profile_total_ms_sum_ = 0.0; - pipeline_worker_render_ms_sum_ = 0.0; - pipeline_worker_total_ms_sum_ = 0.0; - pipeline_worker_samples_ = 0; - } - - return packet_opt; - } - - if (!lua_) return std::nullopt; - - double t = current_time - start_time_; - double dt = current_time - last_time_; - last_time_ = current_time; - - frame_count_++; - if (current_time - last_fps_update_ >= 1.0) - { - current_fps_ = static_cast(frame_count_) / static_cast(current_time - last_fps_update_); - frame_count_ = 0; - last_fps_update_ = current_time; - } - - (*lua_)["time"] = t; - (*lua_)["dt"] = dt; - const double globals_end = get_time_sec(); - const double globals_ms = (globals_end - total_start) * 1000.0; - - double render_ms = 0.0; - sol::protected_function render_fn = (*lua_)["render"]; - if (render_fn.valid()) - { - const double render_start = get_time_sec(); - auto result = render_fn(); - const double render_end = get_time_sec(); - render_ms = (render_end - render_start) * 1000.0; - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[ScriptedScenesDesktop:{}] render() error: {}", current_scene_name_, err.what()); - return std::nullopt; - } - } - - const double packet_start = get_time_sec(); - blit_script_canvas_to_output_locked(); - auto packet = std::unique_ptr( - new ScriptedScenesPacket(canvas_data_),[](UdpPacket* p) { delete dynamic_cast(p); } - ); - const double packet_end = get_time_sec(); - const double packet_ms = (packet_end - packet_start) * 1000.0; - const double total_ms = (packet_end - total_start) * 1000.0; - - profile_frames_++; - profile_globals_ms_sum_ += globals_ms; - profile_lua_render_ms_sum_ += render_ms; - profile_packet_ms_sum_ += packet_ms; - profile_total_ms_sum_ += total_ms; - - if (profile_window_start_ <= 0.0) - profile_window_start_ = current_time; - - if (current_time - profile_window_start_ >= 1.0) - { - if (profile_frames_ > 0) - { - const float frames = static_cast(profile_frames_); - profile_avg_globals_ms_ = static_cast(profile_globals_ms_sum_ / frames); - profile_avg_lua_render_ms_ = static_cast(profile_lua_render_ms_sum_ / frames); - profile_avg_packet_ms_ = static_cast(profile_packet_ms_sum_ / frames); - profile_avg_total_ms_ = static_cast(profile_total_ms_sum_ / frames); - profile_avg_set_pixel_calls_per_frame_ = static_cast(profile_set_pixel_calls_) / frames; - profile_avg_clear_calls_per_frame_ = static_cast(profile_clear_calls_) / frames; - } - else - { - profile_avg_globals_ms_ = 0.0f; - profile_avg_lua_render_ms_ = 0.0f; - profile_avg_packet_ms_ = 0.0f; - profile_avg_total_ms_ = 0.0f; - profile_avg_set_pixel_calls_per_frame_ = 0.0f; - profile_avg_clear_calls_per_frame_ = 0.0f; - } - - spdlog::info( - "[ScriptedScenesDesktop:{}] perf avg: total={:.3f}ms render={:.3f}ms globals={:.3f}ms packet={:.3f}ms set_pixel/frame={:.1f} clear/frame={:.2f} fps={:.1f}", - current_scene_name_, profile_avg_total_ms_, profile_avg_lua_render_ms_, - profile_avg_globals_ms_, profile_avg_packet_ms_, - profile_avg_set_pixel_calls_per_frame_, profile_avg_clear_calls_per_frame_, current_fps_); - - profile_window_start_ = current_time; - profile_frames_ = 0; - profile_set_pixel_calls_ = 0; - profile_clear_calls_ = 0; - profile_globals_ms_sum_ = 0.0; - profile_lua_render_ms_sum_ = 0.0; - profile_packet_ms_sum_ = 0.0; - profile_total_ms_sum_ = 0.0; - } - - return packet; -} \ No newline at end of file diff --git a/plugins/ScriptedScenes/desktop/ScriptedScenesDesktop.h b/plugins/ScriptedScenes/desktop/ScriptedScenesDesktop.h deleted file mode 100644 index 847538d9..00000000 --- a/plugins/ScriptedScenes/desktop/ScriptedScenesDesktop.h +++ /dev/null @@ -1,209 +0,0 @@ -#pragma once - -#include "shared/desktop/plugin/main.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "shared/desktop/utils.h" - -#define SOL_ALL_SAFETIES_ON 1 -#include - -const int MAX_WORKERS = std::thread::hardware_concurrency(); - -class ScriptedScenesPacket : public UdpPacket -{ -public: - std::vector frameData; - - explicit ScriptedScenesPacket(const std::vector &data) : UdpPacket(0x04), frameData(data) - { - } - - [[nodiscard]] std::vector toData() const override - { - return frameData; - } -}; - -class ScriptedScenesDesktop : public Plugins::DesktopPlugin -{ -public: - ScriptedScenesDesktop(); - ~ScriptedScenesDesktop() override; - - void render() override; - std::string get_plugin_name() const override { return "ScriptedScenes"; } - - void on_websocket_message(const std::string message) override; - - std::optional> - compute_next_packet(const std::string sceneName) override; - - void initialize_imgui(ImGuiContext *im_gui_context, ImGuiMemAllocFunc *alloc_fn, ImGuiMemFreeFunc *free_fn, - void **user_data) override; - -private: - struct RenderConfig - { - int matrix_width = 128; - int matrix_height = 128; - int script_width = 128; - int script_height = 128; - std::vector upscale_x_map; - std::vector upscale_y_map; - }; - - struct FrameJob - { - uint64_t frame_index = 0; - double t = 0.0; - double dt = 0.0; - }; - - struct FrameResult - { - uint64_t frame_index = 0; - std::vector frame_data; - uint64_t set_pixel_calls = 0; - uint64_t clear_calls = 0; - double render_ms = 0.0; - double total_ms = 0.0; - }; - - struct WorkerState - { - int id = 0; - std::thread thread; - std::unique_ptr lua; - RenderConfig render_config; - std::vector script_canvas_data; - std::map default_properties; - std::string scene_name; - bool unsafe_mode = false; - bool lua_loaded = false; - uint64_t set_pixel_calls = 0; - uint64_t clear_calls = 0; - - std::atomic stop_requested{false}; - std::atomic is_finished{false}; - }; - - std::unique_ptr lua_; - std::string script_content_; - std::string current_scene_name_; - bool is_lua_loaded_ = false; - bool offload_render_ = true; - bool deterministic_parallel_ = false; - bool bypass_protected_calls_ = false; - bool use_parallel_pipeline_ = false; - - // Desktop canvas buffer - std::vector canvas_data_; - std::vector script_canvas_data_; - std::vector upscale_x_map_; - std::vector upscale_y_map_; - int matrix_width_ = 128; - int matrix_height_ = 128; - int script_width_ = 128; - int script_height_ = 128; - int render_downscale_ = 1; - - std::map default_properties_; - - // Time tracking - double start_time_ = 0.0; - double last_time_ = 0.0; - - // FPS tracking - int frame_count_ = 0; - double last_fps_update_ = 0.0; - float current_fps_ = 0.0f; - - // Lightweight performance profiling (updated once per second) - double profile_window_start_ = 0.0; - uint64_t profile_frames_ = 0; - uint64_t profile_set_pixel_calls_ = 0; - uint64_t profile_clear_calls_ = 0; - double profile_globals_ms_sum_ = 0.0; - double profile_lua_render_ms_sum_ = 0.0; - double profile_packet_ms_sum_ = 0.0; - double profile_total_ms_sum_ = 0.0; - - float profile_avg_globals_ms_ = 0.0f; - float profile_avg_lua_render_ms_ = 0.0f; - float profile_avg_packet_ms_ = 0.0f; - float profile_avg_total_ms_ = 0.0f; - float profile_avg_set_pixel_calls_per_frame_ = 0.0f; - float profile_avg_clear_calls_per_frame_ = 0.0f; - - std::mutex script_mutex_; - std::mutex pipeline_mutex_; - std::condition_variable pipeline_cv_; - - std::vector> workers_; - std::deque frame_jobs_; - std::map completed_frames_; - RenderConfig pipeline_render_config_; - bool stop_workers_ = false; - bool workers_started_ = false; - uint64_t next_schedule_frame_index_ = 0; - uint64_t next_send_frame_index_ = 0; - double pipeline_start_time_ = 0.0; - double missing_frame_since_ = 0.0; - - int pipeline_worker_count_ = std::max(MAX_WORKERS / 2, 1); - int pipeline_lookahead_depth_ = 12; - int pipeline_max_queued_frames_ = 32; - float pipeline_max_reorder_wait_ms_ = 250.0f; - float pipeline_target_fps_ = 60.0f; - - uint64_t pipeline_frames_sent_ = 0; - uint64_t pipeline_frames_dropped_ = 0; - float pipeline_queue_depth_ = 0.0f; - float pipeline_completed_depth_ = 0.0f; - float pipeline_reorder_wait_ms_ = 0.0f; - float pipeline_effective_send_fps_ = 0.0f; - float pipeline_avg_worker_render_ms_ = 0.0f; - float pipeline_avg_worker_total_ms_ = 0.0f; - double pipeline_worker_render_ms_sum_ = 0.0; - double pipeline_worker_total_ms_sum_ = 0.0; - uint64_t pipeline_worker_samples_ = 0; - - // Adaptive pipeline tuning - bool adaptive_pipeline_ = true; - uint64_t adaptive_last_drop_count_ = 0; - int adaptive_stable_seconds_ = 0; - int adaptive_drop_bursts_ = 0; - uint64_t adaptive_drops_last_window_ = 0; - float adaptive_fps_ratio_last_ = 1.0f; - std::string adaptive_last_action_ = "none"; - - void maybe_adapt_pipeline_locked(); - void update_script_dimensions_locked(); - void blit_script_canvas_to_output_locked(); - static void blit_script_canvas_to_output(const std::vector &script_canvas, - std::vector &output_canvas, - const RenderConfig &config); - void setup_lua_state(); - bool load_and_exec_script(const std::string &script_content); - void reset_profiling_locked(double now); - - // Dynamic thread lifecycle - void stop_pipeline_workers_locked(); - bool start_pipeline_workers_locked(); - void sync_pipeline_workers_locked(); - bool init_worker(WorkerState *worker_ctx); - - void maybe_update_pipeline_mode_locked(); - void schedule_pipeline_jobs_locked(); - std::optional try_take_next_pipeline_frame_locked(double now); - void worker_loop(WorkerState *worker); -}; \ No newline at end of file diff --git a/plugins/ScriptedScenes/matrix/LatestLuaScene.cpp b/plugins/ScriptedScenes/matrix/LatestLuaScene.cpp deleted file mode 100644 index e295d28a..00000000 --- a/plugins/ScriptedScenes/matrix/LatestLuaScene.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "LatestLuaScene.h" -#include -#include -#include "shared/common/utils/utils.h" - -namespace fs = std::filesystem; - -static const fs::path lua_scenes_dir = get_exec_dir() / "data" / "custom_lua"; - -namespace Scenes -{ - LatestLuaScene::LatestLuaScene() - { - last_check_time_ = clock::now(); - } - - void LatestLuaScene::register_properties() - { - // We don't register any properties ourselves. - // The wrapped LuaScene will initialize its own properties internally using its defaults. - } - - void LatestLuaScene::initialize(int width, int height) - { - Scene::initialize(width, height); - width_ = width; - height_ = height; - - // Check for newest scene immediately - check_for_updates(); - } - - void LatestLuaScene::check_for_updates() - { - if (!fs::exists(lua_scenes_dir)) return; - - fs::path newest_path; - fs::file_time_type newest_time = fs::file_time_type::min(); - - try - { - for (const auto& entry : fs::directory_iterator(lua_scenes_dir)) - { - if (!entry.is_regular_file()) continue; - if (entry.path().extension() != ".lua") continue; - - auto mtime = fs::last_write_time(entry.path()); - if (mtime > newest_time) - { - newest_time = mtime; - newest_path = entry.path(); - } - } - } - catch (const fs::filesystem_error& e) - { - spdlog::warn("[LatestLuaScene] File system error while scanning dir: {}", e.what()); - return; - } - - if (!newest_path.empty() && (newest_path != current_script_path_ || newest_time > current_write_time_)) - { - spdlog::info("[LatestLuaScene] Switching to latest lua scene: {}", newest_path.filename().string()); - - auto new_scene = std::make_unique(newest_path); - - // Setup the new scene - new_scene->register_properties(); - - // Load default properties so they are marked as 'registered' - nlohmann::json empty_config = nlohmann::json::object(); - for (const auto& prop : new_scene->get_properties()) - { - prop->load_from_json(empty_config); - } - - new_scene->initialize(width_, height_); - - current_scene_ = std::move(new_scene); - current_script_path_ = newest_path; - current_write_time_ = newest_time; - } - } - - bool LatestLuaScene::needs_desktop_app() - { - check_for_updates(); - if (current_scene_) - { - spdlog::info("[LatestLuaScene] Scene has been loaded"); - return current_scene_->needs_desktop_app(); - } - - spdlog::info("Falling back"); - return false; - } - - bool LatestLuaScene::render(rgb_matrix::FrameCanvas* canvas) - { - auto now = clock::now(); - if (std::chrono::duration_cast(now - last_check_time_).count() > 500) - { - check_for_updates(); - last_check_time_ = now; - } - - if (current_scene_) - { - return current_scene_->render(canvas); - } - - // If no scene is available, just clear and keep running - canvas->Clear(); - return true; - } -} // namespace Scenes diff --git a/plugins/ScriptedScenes/matrix/LatestLuaScene.h b/plugins/ScriptedScenes/matrix/LatestLuaScene.h deleted file mode 100644 index b0e52875..00000000 --- a/plugins/ScriptedScenes/matrix/LatestLuaScene.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include "shared/matrix/Scene.h" -#include "shared/matrix/wrappers.h" -#include "LuaScene.h" -#include -#include -#include - -namespace Scenes { - - class LatestLuaScene : public Scene { - public: - LatestLuaScene(); - ~LatestLuaScene() override = default; - - bool render(rgb_matrix::FrameCanvas *canvas) override; - std::string get_name() const override { return "Latest Lua Scene"; } - std::string getCategory() const override { return "Custom Lua"; } - void register_properties() override; - void initialize(int width, int height) override; - - tmillis_t get_default_duration() override { return 10000; } - int get_default_weight() override { return 5; } - - bool needs_desktop_app() override; - - private: - std::unique_ptr current_scene_; - std::filesystem::path current_script_path_; - std::filesystem::file_time_type current_write_time_{}; - - // Cache matrix dimensions - int width_ = 128; - int height_ = 128; - - // Rate limit file system checks - using clock = std::chrono::steady_clock; - clock::time_point last_check_time_; - - void check_for_updates(); - }; - - class LatestLuaSceneWrapper : public Plugins::SceneWrapper { - public: - LatestLuaSceneWrapper() = default; - - std::unique_ptr create() override { - return {new LatestLuaScene(), [](Scenes::Scene *s) { delete s; }}; - } - - std::string get_name() override { return "Latest Lua Scene"; } - }; - -} // namespace Scenes diff --git a/plugins/ScriptedScenes/matrix/LuaScene.cpp b/plugins/ScriptedScenes/matrix/LuaScene.cpp deleted file mode 100644 index 98975529..00000000 --- a/plugins/ScriptedScenes/matrix/LuaScene.cpp +++ /dev/null @@ -1,527 +0,0 @@ -#include "LuaScene.h" -#include "shared/matrix/server/server_utils.h" - -#define SOL_ALL_SAFETIES_ON 1 -#include - -#include "ScriptedScenes.h" -#include "shared/matrix/plugin_loader/loader.h" -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Helper: extract the Lua `name` global from a file without keeping state. -// Returns an empty string if the file cannot be loaded or the global is absent. -// --------------------------------------------------------------------------- -static std::string quick_read_name(const std::filesystem::path& path) -{ - try - { - sol::state tmp; - tmp.open_libraries(sol::lib::base); - auto result = tmp.script_file( - path.string(), - [](lua_State*, sol::protected_function_result pfr) { return pfr; }); - if (!result.valid()) - return ""; - sol::object name_obj = tmp["name"]; - if (name_obj.is()) - return name_obj.as(); - } - catch (...) - { - } - return ""; -} - -namespace Scenes -{ - LuaScene::LuaScene(std::filesystem::path script_path) - : script_path_(std::move(script_path)) - { - scene_name_ = script_path_.stem().string(); - } - - LuaScene::~LuaScene() = default; - - // --------------------------------------------------------------------------- - // Initialise the Lua state and bind all C++ β†’ Lua API functions. - // This is called on first load and on every hot-reload. - // --------------------------------------------------------------------------- - void LuaScene::setup_lua_state() - { - lua_ = std::make_unique(); - lua_->open_libraries(sol::lib::base, sol::lib::math, sol::lib::string, - sol::lib::table); - - // ---- set_pixel(x, y, r, g, b) ---------------------------------------- - lua_->set_function("set_pixel", [this](int x, int y, int r, int g, int b) - { - if (!current_canvas_) - return; - if (x < 0 || x >= matrix_width || y < 0 || y >= matrix_height) - return; - current_canvas_->SetPixel(x, y, static_cast(std::clamp(r, 0, 255)), - static_cast(std::clamp(g, 0, 255)), - static_cast(std::clamp(b, 0, 255))); - }); - - // ---- clear() ---------------------------------------------------------- - lua_->set_function("clear", [this]() - { - if (current_canvas_) - current_canvas_->Clear(); - }); - - // ---- log(msg) --------------------------------------------------------- - lua_->set_function("log", [this](const std::string& msg) - { - spdlog::info("[LuaScene:{}] {}", scene_name_, msg); - }); - - // ---- define_property(name, type, default [, min, max]) ---------------- - lua_->set_function("define_property", [this](const std::string& name, - const std::string& type, - sol::object default_val, - sol::variadic_args va) - { - if (!is_first_load_) - { - // Hot-reload: property shapes are fixed; just ignore re-definitions. - return; - } - - // Check for duplicates (can happen if the script calls setup() twice). - for (const auto& e : lua_props_) - { - if (e.name == name) - return; - } - - try - { - LuaPropEntry entry; - entry.name = name; - entry.type = type; - - if (type == "float") - { - float def = default_val.is() - ? static_cast(default_val.as()) - : 0.f; - std::optional mn, mx; - if (va.size() >= 1) - mn = static_cast(va[0].as()); - if (va.size() >= 2) - mx = static_cast(va[1].as()); - auto prop = std::make_shared>(name, def, false, - mn, mx); - add_property(prop); - entry.prop = prop; - } - else if (type == "int") - { - int def = default_val.is() - ? static_cast(default_val.as()) - : 0; - std::optional mn, mx; - if (va.size() >= 1) - mn = static_cast(va[0].as()); - if (va.size() >= 2) - mx = static_cast(va[1].as()); - auto prop = - std::make_shared>(name, def, false, mn, mx); - add_property(prop); - entry.prop = prop; - } - else if (type == "bool") - { - bool def = default_val.is() ? default_val.as() : false; - auto prop = std::make_shared>(name, def, false); - add_property(prop); - entry.prop = prop; - } - else if (type == "string") - { - std::string def = - default_val.is() ? default_val.as() : ""; - auto prop = - std::make_shared>(name, def, false); - add_property(prop); - entry.prop = prop; - } - else if (type == "color") - { - // Lua passes colors as 0xRRGGBB integers. - uint32_t raw = default_val.is() - ? static_cast(default_val.as()) - : 0u; - rgb_matrix::Color def{ - static_cast((raw >> 16) & 0xFF), - static_cast((raw >> 8) & 0xFF), - static_cast(raw & 0xFF) - }; - auto prop = std::make_shared>( - name, def, false); - add_property(prop); - entry.prop = prop; - } - else - { - spdlog::warn( - "[LuaScene:{}] Unknown property type '{}' for '{}', skipping", - scene_name_, type, name); - return; - } - - lua_props_.push_back(std::move(entry)); - } - catch (const std::exception& ex) - { - spdlog::error("[LuaScene:{}] define_property('{}') failed: {}", - scene_name_, name, ex.what()); - } - }); - - // ---- get_property(name) β†’ number | string | bool | nil --------------- - lua_->set_function( - "get_property", [this](const std::string& name) -> sol::object - { - for (const auto& e : lua_props_) - { - if (e.name != name) - continue; - - if (e.type == "float") - { - auto p = std::static_pointer_cast>(e.prop); - return sol::make_object(*lua_, static_cast(p->get())); - } - if (e.type == "int") - { - auto p = std::static_pointer_cast>(e.prop); - return sol::make_object(*lua_, static_cast(p->get())); - } - if (e.type == "bool") - { - auto p = std::static_pointer_cast>(e.prop); - return sol::make_object(*lua_, p->get()); - } - if (e.type == "string") - { - auto p = std::static_pointer_cast>( - e.prop); - return sol::make_object(*lua_, p->get()); - } - if (e.type == "color") - { - auto p = - std::static_pointer_cast>( - e.prop); - auto c = p->get(); - uint32_t val = (static_cast(c.r) << 16) | - (static_cast(c.g) << 8) | - static_cast(c.b); - return sol::make_object(*lua_, static_cast(val)); - } - } - return sol::nil; - }); - - // ---- static width / height globals (updated in initialize) ----------- - (*lua_)["width"] = matrix_width; - (*lua_)["height"] = matrix_height; - (*lua_)["time"] = 0.0; - (*lua_)["dt"] = 0.0; - } - - // --------------------------------------------------------------------------- - bool LuaScene::load_and_exec_script() - { - auto result = lua_->script_file( - script_path_.string(), - [](lua_State*, sol::protected_function_result pfr) { return pfr; }); - - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[LuaScene:{}] Script load error: {}", scene_name_, - err.what()); - lua_loaded_ = false; - return false; - } - - // Override scene name from Lua global if present. - sol::object name_obj = (*lua_)["name"]; - if (name_obj.is()) - { - scene_name_ = name_obj.as(); - } - - sol::object offload_obj = (*lua_)["offload"]; - if (offload_obj.is()) - { - offload_render_ = offload_obj.as(); - } - else - { - offload_render_ = true; - } - - sol::object external_render_only_obj = (*lua_)["external_render_only"]; - if (external_render_only_obj.is()) - { - external_render_only_ = external_render_only_obj.as(); - } - else - { - external_render_only_ = false; - } - - try - { - last_write_time_ = std::filesystem::last_write_time(script_path_); - } - catch (...) - { - } - - static ScriptedScenes* plugin = nullptr; - if (!plugin) - { - auto plugins = Plugins::PluginManager::instance()->get_plugins(); - for (auto& p : plugins) - { - if (auto v = dynamic_cast(p)) - { - plugin = v; - break; - } - } - } - - if (offload_render_) - { - // Send script to desktop - std::ifstream file(script_path_); - if (file) - { - std::stringstream buffer; - buffer << file.rdbuf(); - std::string content = buffer.str(); - - if (plugin) - { - plugin->send_msg_to_desktop("script:" + scene_name_ + ":" + - content); - plugin->set_active_script(script_path_, scene_name_); - } - else - { - spdlog::warn( - "ScriptedScenes plugin not found, cannot send script to desktop"); - } - } - } - - lua_loaded_ = true; - return true; - } - - // --------------------------------------------------------------------------- - void LuaScene::call_setup() - { - sol::protected_function fn = (*lua_)["setup"]; - if (!fn.valid()) - return; - - auto result = fn(); - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[LuaScene:{}] setup() error: {}", scene_name_, err.what()); - } - } - - void LuaScene::call_initialize_fn() - { - sol::protected_function fn = (*lua_)["initialize"]; - if (!fn.valid()) - return; - - auto result = fn(); - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[LuaScene:{}] initialize() error: {}", scene_name_, - err.what()); - } - } - - bool LuaScene::had_changes_to_script() const - { - try - { - auto mtime = std::filesystem::last_write_time(script_path_); - return mtime != last_write_time_; - } - catch (const std::filesystem::filesystem_error&) - { - // File briefly unavailable (e.g. being written) – assume no changes. - return false; - } - } - - bool LuaScene::needs_desktop_app() - { - return external_render_only_ && !had_changes_to_script(); - } - - // --------------------------------------------------------------------------- - void LuaScene::register_properties() - { - setup_lua_state(); - if (load_and_exec_script()) - { - call_setup(); - } - is_first_load_ = false; - } - - // --------------------------------------------------------------------------- - void LuaScene::initialize(int width, int height) - { - Scene::initialize(width, height); - - // Update width/height globals in the Lua state. - if (lua_) - { - (*lua_)["width"] = matrix_width; - (*lua_)["height"] = matrix_height; - } - - call_initialize_fn(); - } - - // --------------------------------------------------------------------------- - bool LuaScene::render(rgb_matrix::FrameCanvas* canvas) - { - if (!lua_) - return true; - - // We can fetch the plugin instance once to avoid doing it every frame - static ScriptedScenes* plugin = nullptr; - if (!plugin) - { - auto plugins = Plugins::PluginManager::instance()->get_plugins(); - for (auto& p : plugins) - { - if (auto v = dynamic_cast(p)) - { - plugin = v; - break; - } - } - } - - // --- hot-reload: check if the script file has changed ----------------- - try - { - if (had_changes_to_script()) - { - spdlog::info("[LuaScene] Hot-reloading '{}'", - script_path_.filename().string()); - setup_lua_state(); - if (load_and_exec_script()) - { - call_setup(); - // Re-run initialize with existing dimensions. - (*lua_)["width"] = matrix_width; - (*lua_)["height"] = matrix_height; - call_initialize_fn(); - } - // Reset timer so `time` restarts from 0 after a reload. - frame_timer_ = FrameTimer{}; - } - } - catch (const std::filesystem::filesystem_error&) - { - // File briefly unavailable (e.g. being written) – skip this frame. - return true; - } - - if (!lua_loaded_) - return true; - - auto frame = frame_timer_.tick(); - (*lua_)["time"] = frame.t; - (*lua_)["dt"] = frame.dt; - - if (offload_render_) - { - if (Server::is_desktop_connected()) - { - // Desktop is rendering this, so we just return true. - // The actual drawing to canvas happens via ScriptedScenes::on_udp_packet. - if (plugin) - { - const auto pixels = plugin->get_data(); - if (!pixels.empty()) - { - const uint8_t* data = pixels.data(); - const int max_pixels = pixels.size() / 3; - const int limit = std::min(matrix_width * matrix_height, max_pixels); - - for (int idx = 0; idx < limit; ++idx) - { - int x = idx % matrix_width; - int y = idx / matrix_width; - int i = idx * 3; - canvas->SetPixel(x, y, data[i], data[i + 1], data[i + 2]); - } - } - } - return true; - } - - if (external_render_only_) - { - return false; - } - } - - sol::protected_function render_fn = (*lua_)["render"]; - if (!render_fn.valid()) - return true; - - current_canvas_ = canvas; - auto result = render_fn(); - current_canvas_ = nullptr; - - if (offload_render_) - { -#ifdef ENABLE_EMULATOR - canvas->SetPixel(0, 0, 255, 0, 0); - canvas->SetPixel(0, 1, 0, 255, 0); - canvas->SetPixel(1, 1, 0, 0, 255); - canvas->SetPixel(1, 0, 255, 255, 0); -#endif - } - - if (!result.valid()) - { - sol::error err = result; - spdlog::error("[LuaScene:{}] render() error: {}", scene_name_, err.what()); - return true; // keep running despite errors - } - - // render() should return true to keep running, false to stop. - sol::object ret_val = result; - if (ret_val.is()) - return ret_val.as(); - return true; - } -} // namespace Scenes diff --git a/plugins/ScriptedScenes/matrix/LuaScene.h b/plugins/ScriptedScenes/matrix/LuaScene.h deleted file mode 100644 index 4831327a..00000000 --- a/plugins/ScriptedScenes/matrix/LuaScene.h +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once - -#include "shared/matrix/Scene.h" -#include "shared/matrix/wrappers.h" -#include "shared/matrix/utils/FrameTimer.h" - -#include -#include -#include -#include -#include - -// Forward-declare sol2 types to keep the header clean (sol.hpp is large). -namespace sol -{ - class state; -} - -namespace Scenes -{ - /// One Lua scene instance wrapping a single .lua script file. - class LuaScene : public Scene - { - public: - explicit LuaScene(std::filesystem::path script_path); - ~LuaScene() override; - - bool render(rgb_matrix::FrameCanvas* canvas) override; - std::string get_name() const override { return scene_name_; } - std::string getCategory() const override { return "Custom Lua"; } - void register_properties() override; - void initialize(int width, int height) override; - - tmillis_t get_default_duration() override { return 10000; } - int get_default_weight() override { return 5; } - - [[nodiscard]] bool needs_desktop_app() override; - - private: - std::filesystem::path script_path_; - std::string scene_name_; - - std::unique_ptr lua_; - FrameTimer frame_timer_; - - std::filesystem::file_time_type last_write_time_{}; - bool lua_loaded_ = false; - bool is_first_load_ = true; - bool offload_render_ = false; - bool external_render_only_ = false; - - /// Canvas pointer valid only inside render(); used by set_pixel / clear callbacks. - rgb_matrix::FrameCanvas* current_canvas_ = nullptr; - - /// Tracks dynamic property name β†’ type string so get_property can cast correctly. - struct LuaPropEntry - { - std::string name; - std::string type; - std::shared_ptr prop; - }; - - std::vector lua_props_; - - void setup_lua_state(); - bool load_and_exec_script(); - bool had_changes_to_script() const; - void call_setup(); - void call_initialize_fn(); - - /// Read a property value and return it as a Lua-compatible number or string. - double get_prop_as_number(const LuaPropEntry& entry) const; - std::string get_prop_as_string(const LuaPropEntry& entry) const; - }; - - /// SceneWrapper that instantiates LuaScene for a given script path. - class LuaSceneWrapper : public Plugins::SceneWrapper - { - public: - LuaSceneWrapper(std::filesystem::path path, std::string name) - : script_path_(std::move(path)), cached_name_(std::move(name)) - { - } - - std::unique_ptr create() override - { - return {new LuaScene(script_path_), [](Scenes::Scene* s) { delete s; }}; - } - - /// Return the pre-scanned name so we don't need to spin up a Lua state - /// just to populate the scene list. - std::string get_name() override { return cached_name_; } - - private: - std::filesystem::path script_path_; - std::string cached_name_; - }; - - class CustomLuaScene final : public LuaScene - { - public: - explicit CustomLuaScene(std::filesystem::path script_path) : LuaScene(std::move(script_path)) {} - std::string getCategory() const override { return "Custom Lua"; } - }; - - class CustomLuaSceneWrapper : public Plugins::SceneWrapper - { - public: - CustomLuaSceneWrapper(std::filesystem::path path, std::string name) - : script_path_(std::move(path)), cached_name_(std::move(name)) - { - } - - std::unique_ptr create() override - { - return {new CustomLuaScene(script_path_), [](Scenes::Scene* s) { delete s; }}; - } - - std::string get_name() override { return cached_name_; } - - private: - std::filesystem::path script_path_; - std::string cached_name_; - }; -} // namespace Scenes diff --git a/plugins/ScriptedScenes/matrix/ScriptedScenes.cpp b/plugins/ScriptedScenes/matrix/ScriptedScenes.cpp deleted file mode 100644 index e4f3a43a..00000000 --- a/plugins/ScriptedScenes/matrix/ScriptedScenes.cpp +++ /dev/null @@ -1,293 +0,0 @@ -#include "ScriptedScenes.h" - -#define SOL_ALL_SAFETIES_ON 1 -#include - -#include "LatestLuaScene.h" -#include "shared/matrix/plugin_loader/loader.h" -#include "shared/common/utils/utils.h" -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -// --------------------------------------------------------------------------- -// The directory scanned for .lua scene scripts. -// Resolved relative to the process working directory, which is the same root -// used for Constants::root_dir ("images/"). -// --------------------------------------------------------------------------- -static const fs::path lua_scenes_dir = get_exec_dir() / "data" / "custom_lua"; - -extern "C" PLUGIN_EXPORT ScriptedScenes *createScriptedScenes() { - return new ScriptedScenes(); -} - -extern "C" PLUGIN_EXPORT void destroyScriptedScenes(ScriptedScenes *c) { - delete c; -} - -vector< - std::unique_ptr> -ScriptedScenes::create_scenes() { - using WrapperPtr = - std::unique_ptr; - vector scenes; - - if (!fs::exists(lua_scenes_dir)) { - std::error_code ec; - fs::create_directories(lua_scenes_dir, ec); - if (ec) { - spdlog::warn( - "[ScriptedScenes] Could not create lua_scenes directory '{}': {}", - lua_scenes_dir.string(), ec.message()); - } else { - spdlog::info("[ScriptedScenes] Created lua_scenes directory at '{}'", - lua_scenes_dir.string()); - } - return scenes; - } - - auto deleter = [](Plugins::SceneWrapper *w) { delete w; }; - - for (const auto &entry : fs::directory_iterator(lua_scenes_dir)) { - if (!entry.is_regular_file()) - continue; - if (entry.path().extension() != ".lua") - continue; - - const fs::path &path = entry.path(); - std::string name = path.stem().string(); - - // Quick-load the script to read the optional `name` global without - // keeping a full Lua state alive. - { - try { - sol::state tmp; - tmp.open_libraries(sol::lib::base); - auto res = tmp.script_file( - path.string(), [](lua_State *, sol::protected_function_result pfr) { - return pfr; - }); - if (res.valid()) { - sol::object n = tmp["name"]; - if (n.is()) - name = n.as(); - } else { - sol::error err = res; - spdlog::warn("[ScriptedScenes] Skipping '{}' (parse error): {}", - path.filename().string(), err.what()); - continue; - } - } catch (const std::exception &ex) { - spdlog::warn("[ScriptedScenes] Skipping '{}': {}", - path.filename().string(), ex.what()); - continue; - } - } - - spdlog::info("[ScriptedScenes] Registering Lua scene '{}' from '{}'", name, - path.filename().string()); - - try { - known_files_[name] = fs::last_write_time(path); - } catch (...) { - known_files_[name] = std::filesystem::file_time_type::min(); - } - - scene_name_by_path_[path.string()] = name; - scenes.emplace_back(new Scenes::CustomLuaSceneWrapper(path, name), deleter); - } - - scenes.emplace_back(new Scenes::LatestLuaSceneWrapper(), deleter); - - return scenes; -} - -std::optional ScriptedScenes::after_server_init() { - stop_watcher_ = false; - watcher_thread_ = std::thread(&ScriptedScenes::watch_directory, this); - return std::nullopt; -} - -std::optional ScriptedScenes::pre_exit() { - stop_watcher_ = true; - if (watcher_thread_.joinable()) { - watcher_thread_.join(); - } - return std::nullopt; -} - -void ScriptedScenes::watch_directory() { - while (!stop_watcher_) { - for (int i = 0; i < 10 && !stop_watcher_; i++) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - if (stop_watcher_) - break; - - if (!fs::exists(lua_scenes_dir)) - continue; - - std::unordered_map found_files; - auto deleter = [](Plugins::SceneWrapper *w) { delete w; }; - - for (const auto &entry : fs::directory_iterator(lua_scenes_dir)) { - if (!entry.is_regular_file()) - continue; - if (entry.path().extension() != ".lua") - continue; - - const fs::path &path = entry.path(); - std::string name = path.stem().string(); - - // Quick-load the script to read the optional `name` global - { - try { - sol::state tmp; - tmp.open_libraries(sol::lib::base); - auto res = tmp.script_file( - path.string(), - [](lua_State *, sol::protected_function_result pfr) { - return pfr; - }); - if (res.valid()) { - sol::object n = tmp["name"]; - if (n.is()) - name = n.as(); - } - } catch (...) { - } - } - - found_files[name] = true; - - if (known_files_.find(name) == known_files_.end()) { - try { - known_files_[name] = fs::last_write_time(path); - } catch (...) { - known_files_[name] = std::filesystem::file_time_type::min(); - } - - spdlog::info( - "[ScriptedScenes] Runtime loading new Lua scene '{}' from '{}'", - name, path.filename().string()); - - scene_name_by_path_[path.string()] = name; - std::shared_ptr wrapper( - new Scenes::CustomLuaSceneWrapper(path, name), deleter); - - Plugins::PluginManager::instance()->add_scene(std::move(wrapper)); - } - } - - // Check for deletions - for (auto it = known_files_.begin(); it != known_files_.end();) { - if (found_files.find(it->first) == found_files.end()) { - spdlog::info("[ScriptedScenes] Unloading deleted Lua scene '{}'", - it->first); - Plugins::PluginManager::instance()->remove_scene(it->first); - for (auto itr = scene_name_by_path_.begin(); itr != scene_name_by_path_.end();) { - if (itr->second == it->first) { - itr = scene_name_by_path_.erase(itr); - } else { - ++itr; - } - } - it = known_files_.erase(it); - } else { - ++it; - } - } - } -} - -bool ScriptedScenes::on_udp_packet(const uint8_t pluginId, - const uint8_t *packetData, - const size_t size) { - if (pluginId != 0x04) - return false; - std::lock_guard lock(dataMutex); - data.assign(packetData, packetData + size); - return true; -} - -std::vector ScriptedScenes::get_data() { - std::lock_guard lock(dataMutex); - return data; -} - -void ScriptedScenes::set_active_script(std::filesystem::path script_file_path, - std::string sceneName) { - std::lock_guard lock(activeScriptPathMutex); - active_script_path = script_file_path; - active_scene_name = sceneName; -} - -std::optional> ScriptedScenes::on_websocket_open() { - std::vector messages; - - std::lock_guard lock(activeScriptPathMutex); - if (fs::exists(active_script_path)) { - // Read file content - std::ifstream file(active_script_path); - if (file) { - std::stringstream buffer; - buffer << file.rdbuf(); - messages.push_back("script:" + active_scene_name + ":" + buffer.str()); - } - } - - return messages.empty() ? std::nullopt - : std::optional>(messages); -} - -std::string ScriptedScenes::add_custom_lua_scene(const std::filesystem::path &script_path) { - std::string name = script_path.stem().string(); - if (!fs::exists(script_path)) { - return name; - } - - try { - sol::state tmp; - tmp.open_libraries(sol::lib::base); - auto res = tmp.script_file( - script_path.string(), [](lua_State *, sol::protected_function_result pfr) { - return pfr; - }); - if (res.valid()) { - sol::object n = tmp["name"]; - if (n.is()) - name = n.as(); - } - } catch (...) { - } - - std::error_code ec; - known_files_[name] = fs::last_write_time(script_path, ec); - if (ec) { - known_files_[name] = std::filesystem::file_time_type::min(); - } - scene_name_by_path_[script_path.string()] = name; - - auto deleter = [](Plugins::SceneWrapper *w) { delete w; }; - std::shared_ptr wrapper(new Scenes::CustomLuaSceneWrapper(script_path, name), deleter); - Plugins::PluginManager::instance()->add_scene(std::move(wrapper)); - spdlog::info("[ScriptedScenes] Runtime loading new Lua scene '{}' from '{}'", name, script_path.filename().string()); - return name; -} - -std::string ScriptedScenes::remove_custom_lua_scene(const std::filesystem::path &script_path) { - const auto key = script_path.string(); - std::string scene_name = script_path.stem().string(); - if (scene_name_by_path_.contains(key)) { - scene_name = scene_name_by_path_[key]; - scene_name_by_path_.erase(key); - } - known_files_.erase(scene_name); - Plugins::PluginManager::instance()->remove_scene(scene_name); - spdlog::info("[ScriptedScenes] Runtime removed Lua scene '{}' from '{}'", scene_name, script_path.filename().string()); - return scene_name; -} diff --git a/plugins/ScriptedScenes/matrix/ScriptedScenes.h b/plugins/ScriptedScenes/matrix/ScriptedScenes.h deleted file mode 100644 index 5c514a29..00000000 --- a/plugins/ScriptedScenes/matrix/ScriptedScenes.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "shared/matrix/plugin/main.h" -#include -#include -#include -#include - -class ScriptedScenes : public Plugins::BasicPlugin { -public: - ScriptedScenes() = default; - - std::string get_plugin_name() const override { return PLUGIN_NAME; } - - vector> - create_image_providers() override { - return {}; - } - - vector< - std::unique_ptr> - create_scenes() override; - - std::optional after_server_init() override; - std::optional pre_exit() override; - - bool on_udp_packet(const uint8_t pluginId, const uint8_t *packetData, - const size_t size) override; - std::optional> on_websocket_open() override; - - std::vector get_data(); - void set_active_script(std::filesystem::path script_file_path, std::string sceneName); - std::string add_custom_lua_scene(const std::filesystem::path &script_path); - std::string remove_custom_lua_scene(const std::filesystem::path &script_path); - -private: - std::thread watcher_thread_; - std::atomic stop_watcher_{false}; - std::unordered_map known_files_; - std::unordered_map scene_name_by_path_; - - std::mutex dataMutex; - std::vector data; - - std::mutex activeScriptPathMutex; - std::string active_scene_name; - std::filesystem::path active_script_path; - - void watch_directory(); -}; diff --git a/plugins/ScriptedScenes/matrix/examples/colour_bars.lua b/plugins/ScriptedScenes/matrix/examples/colour_bars.lua deleted file mode 100644 index d5b287f6..00000000 --- a/plugins/ScriptedScenes/matrix/examples/colour_bars.lua +++ /dev/null @@ -1,28 +0,0 @@ --- Bouncing colour bars -name = "lua_colour_bars" - -function setup() - define_property("bar_count", "int", 8, 1, 32) - define_property("speed", "float", 1.0, 0.1, 5.0) -end - -function render() - local bars = get_property("bar_count") - local spd = get_property("speed") - local t = time * spd - - for y = 0, height - 1 do - for x = 0, width - 1 do - -- Which bar column is this pixel in? - local bar = math.floor(x / width * bars) - local phase = bar / bars * math.pi * 2.0 + t - - local r = math.floor((math.sin(phase) + 1.0) * 127) - local g = math.floor((math.sin(phase + 2.094) + 1.0) * 127) - local b = math.floor((math.sin(phase + 4.189) + 1.0) * 127) - - set_pixel(x, y, r, g, b) - end - end - return true -end diff --git a/plugins/ScriptedScenes/matrix/examples/plasma.lua b/plugins/ScriptedScenes/matrix/examples/plasma.lua deleted file mode 100644 index b2211692..00000000 --- a/plugins/ScriptedScenes/matrix/examples/plasma.lua +++ /dev/null @@ -1,33 +0,0 @@ --- Plasma wave effect --- A classic demo-scene style plasma that cycles through colours. -name = "lua_plasma" - -function setup() - define_property("speed", "float", 1.0, 0.1, 5.0) - define_property("scale", "float", 0.15, 0.05, 0.5) -end - -function render() - local t = time * get_property("speed") - local sc = get_property("scale") - - for y = 0, height - 1 do - for x = 0, width - 1 do - local v = math.sin(x * sc + t) - + math.sin(y * sc + t * 0.7) - + math.sin((x + y) * sc * 0.5 + t * 1.3) - - -- Map -3..3 β†’ 0..1 - local norm = (v + 3.0) / 6.0 - - -- HSV-style hue rotation: split into R/G/B phases - local phase = norm * math.pi * 2.0 - local r = math.floor((math.sin(phase) + 1.0) * 127) - local g = math.floor((math.sin(phase + 2.094) + 1.0) * 127) - local b = math.floor((math.sin(phase + 4.189) + 1.0) * 127) - - set_pixel(x, y, r, g, b) - end - end - return true -end diff --git a/plugins/ScriptedScenes/matrix/examples/ripple.lua b/plugins/ScriptedScenes/matrix/examples/ripple.lua deleted file mode 100644 index 2f5794a0..00000000 --- a/plugins/ScriptedScenes/matrix/examples/ripple.lua +++ /dev/null @@ -1,58 +0,0 @@ --- Ripple / pond-drop effect --- Concentric rings radiate from a point that bounces around the canvas. -name = "lua_ripple" - -local cx, cy -- current centre of the ripple -local vx, vy -- velocity of the centre (pixels / second) - -function setup() - define_property("speed", "float", 1.0, 0.1, 5.0) - define_property("frequency", "float", 0.3, 0.05, 1.0) - define_property("tint", "color", 0x00BFFF) -end - -function initialize() - math.randomseed(42) - cx = width / 2 - cy = height / 2 - vx = 18 + math.random() * 14 -- ~18-32 px/s - vy = 14 + math.random() * 14 -end - -function render() - local spd = get_property("speed") - local freq = get_property("frequency") - local raw = get_property("tint") - local tr = math.floor(raw / 65536) % 256 - local tg = math.floor(raw / 256) % 256 - local tb = raw % 256 - - -- Move the centre, bounce off walls - cx = cx + vx * dt - cy = cy + vy * dt - if cx < 0 then cx = -cx; vx = -vx end - if cx > width-1 then cx = 2*(width-1) - cx; vx = -vx end - if cy < 0 then cy = -cy; vy = -vy end - if cy > height-1 then cy = 2*(height-1) - cy; vy = -vy end - - local t = time * spd - - for y = 0, height - 1 do - for x = 0, width - 1 do - local dx = x - cx - local dy = y - cy - local dist = math.sqrt(dx*dx + dy*dy) - - -- Cosine wave that decays with distance - local wave = math.cos(dist * freq * math.pi * 2 - t * 6) - local decay = math.max(0, 1 - dist / (width * 0.6)) - local bright = (wave + 1) * 0.5 * decay - - local r = math.floor(tr * bright) - local g = math.floor(tg * bright) - local b = math.floor(tb * bright) - set_pixel(x, y, r, g, b) - end - end - return true -end diff --git a/plugins/ScriptedScenes/matrix/examples/starfield.lua b/plugins/ScriptedScenes/matrix/examples/starfield.lua deleted file mode 100644 index e503cf7f..00000000 --- a/plugins/ScriptedScenes/matrix/examples/starfield.lua +++ /dev/null @@ -1,49 +0,0 @@ --- Starfield / warp-speed effect -name = "lua_starfield" - -local stars = {} -local NUM_STARS = 80 - -function setup() - define_property("speed", "float", 0.5, 0.05, 3.0) -end - -function initialize() - math.randomseed(42) - stars = {} - for i = 1, NUM_STARS do - stars[i] = { - x = (math.random() - 0.5) * width, - y = (math.random() - 0.5) * height, - z = math.random(), -- depth 0..1 - } - end -end - -function render() - clear() - local spd = get_property("speed") - local cx = width / 2 - local cy = height / 2 - - for i = 1, NUM_STARS do - local s = stars[i] - -- Move star toward viewer (z decreases) - s.z = s.z - dt * spd - if s.z <= 0 then - s.x = (math.random() - 0.5) * width - s.y = (math.random() - 0.5) * height - s.z = 1.0 - end - - -- Project 3-D position onto 2-D screen - local sx = math.floor(s.x / s.z + cx) - local sy = math.floor(s.y / s.z + cy) - - -- Brightness: bright when close (small z) - local bright = math.floor((1.0 - s.z) * 255) - - set_pixel(sx, sy, bright, bright, bright) - end - return true -end diff --git a/react-web/package.json b/react-web/package.json index 69449f50..8af12415 100644 --- a/react-web/package.json +++ b/react-web/package.json @@ -33,7 +33,6 @@ "sonner": "^2.0.7", "tailwind-merge": "^2.6.1", "uuid": "^14.0.0", - "wasmoon": "^1.16.0", "zustand": "^5.0.13" }, "devDependencies": { diff --git a/react-web/pnpm-lock.yaml b/react-web/pnpm-lock.yaml index ee90caff..9894f6d8 100644 --- a/react-web/pnpm-lock.yaml +++ b/react-web/pnpm-lock.yaml @@ -77,9 +77,6 @@ importers: uuid: specifier: ^14.0.0 version: 14.0.0 - wasmoon: - specifier: ^1.16.0 - version: 1.16.0 zustand: specifier: ^5.0.13 version: 5.0.13(@types/react@19.2.14)(react@19.2.6) @@ -1492,9 +1489,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/emscripten@1.39.10': - resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} - '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} @@ -2754,10 +2748,6 @@ packages: yaml: optional: true - wasmoon@1.16.0: - resolution: {integrity: sha512-FlRLb15WwAOz1A9OQDbf6oOKKSiefi5VK0ZRF2wgH9xk3o5SnU11tNPaOnQuAh1Ucr66cwwvVXaeVRaFdRBt5g==} - hasBin: true - webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -4252,8 +4242,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@types/emscripten@1.39.10': {} - '@types/estree@0.0.39': {} '@types/estree@1.0.8': {} @@ -5605,10 +5593,6 @@ snapshots: jiti: 1.21.7 terser: 5.46.2 - wasmoon@1.16.0: - dependencies: - '@types/emscripten': 1.39.10 - webidl-conversions@4.0.2: {} whatwg-url@7.1.0: diff --git a/react-web/src/components/AssetManager/LuaPreview.tsx b/react-web/src/components/AssetManager/LuaPreview.tsx deleted file mode 100644 index c3345bc9..00000000 --- a/react-web/src/components/AssetManager/LuaPreview.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useEffect, useRef, useState } from 'react' -import { LuaEngine, LuaFactory } from 'wasmoon' -interface LuaPreviewProps { - apiUrl: string - filename: string | null - script?: string | null -} - -const PREVIEW_SIZE = 128 -const PIXEL_SCALE = 3 - -export default function LuaPreview({ apiUrl, filename, script }: LuaPreviewProps) { - const canvasRef = useRef(null) - const [error, setError] = useState(null) - const [status, setStatus] = useState('Select a Lua script to preview') - - useEffect(() => { - let rafId = 0 - let cancelled = false - const canvas = canvasRef.current - const ctx = canvas?.getContext('2d') - if (!canvas || !ctx || (!filename && !script)) { - return - } - - const width = PREVIEW_SIZE - const height = PREVIEW_SIZE - - let lua: LuaEngine | null = null - const start = async () => { - try { - setError(null) - setStatus(script ? `Previewing ${filename ?? 'local script'}` : `Loading ${filename}...`) - let scriptText: string | null = null - if (script) { - scriptText = script - } else if (filename != null) { - const res = await fetch(`${apiUrl}/api/custom-assets/lua/${encodeURIComponent(filename)}/download`) - if (!res.ok) { - throw new Error(`Failed to fetch script (${res.status})`) - } - scriptText = await res.text() - } else { - console.warn('No script or filename provided for LuaPreview') - } - if (cancelled) return - - const factory = new LuaFactory() - lua = await factory.createEngine() - lua.global.set("log", (msg: string) => console.log(msg)) - lua.global.set("set_pixel", (x: number, y: number, r: number, g: number, b: number) => { - ctx.fillStyle = `rgb(${Math.max(0, Math.min(255, r))}, ${Math.max(0, Math.min(255, g))}, ${Math.max(0, Math.min(255, b))})` - ctx.fillRect(x * PIXEL_SCALE, y * PIXEL_SCALE, PIXEL_SCALE, PIXEL_SCALE) - }) - - lua.global.set("clear", () => { - ctx.fillStyle = 'black' - ctx.fillRect(0, 0, width * PIXEL_SCALE, height * PIXEL_SCALE) - }) - - let properties: { [key: string]: any } = {} - lua.global.set("define_property", (name: string, type: string, defaultValue: any) => { - properties[name] = defaultValue - }) - - lua.global.set("get_property", (name: string) => { - return properties[name] - }) - - lua.global.set("width", width) - lua.global.set("height", height) - - lua.global.set("time", 0) - lua.global.set("dt", 0) - - console.log('Executing Lua script...') - lua.doStringSync(scriptText!) - setStatus(`Previewing ${filename ?? 'local script'}`) - - const renderFn = lua.global.get('render') - lua.global.get("setup")() - lua.global.get("initialize")() - - let currentTime = performance.now() - const tick = () => { - if (cancelled) return - - let delta = performance.now() - currentTime - currentTime = performance.now() - - lua!.global.set("time", currentTime / 1000) - lua!.global.set("dt", delta / 1000) - - renderFn() - rafId = requestAnimationFrame(tick) - } - rafId = requestAnimationFrame(tick) - } catch (e: any) { - setError(e?.message ?? 'Failed to load Lua script') - } - } - - start() - - return () => { - cancelled = true - cancelAnimationFrame(rafId) - if(lua && !lua.global.isClosed()) { - lua.global.close() - } - } - }, [apiUrl, filename, script]) - - return ( -
-
- -
-

{status}

- {error &&

{error}

} -
- ) -} - diff --git a/react-web/src/pages/AssetManager.tsx b/react-web/src/pages/AssetManager.tsx index c54b9866..0e7bad27 100644 --- a/react-web/src/pages/AssetManager.tsx +++ b/react-web/src/pages/AssetManager.tsx @@ -3,13 +3,10 @@ import { Download, Trash2, Upload } from 'lucide-react' import { Button } from '~/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '~/components/ui/dialog' import { useApiUrl } from '~/components/apiUrl/ApiUrlProvider' -import LuaPreview from '~/components/AssetManager/LuaPreview' import ShaderPreview from '~/components/AssetManager/ShaderPreview' -type AssetType = 'lua' | 'shader' - interface CustomAsset { filename: string size: number @@ -18,35 +15,25 @@ interface CustomAsset { export default function AssetManager() { const apiUrl = useApiUrl() - const [tab, setTab] = useState('lua') const [assets, setAssets] = useState([]) const [loading, setLoading] = useState(false) - const [selectedLua, setSelectedLua] = useState(null) const [selectedShader, setSelectedShader] = useState(null) const fileInputRef = useRef(null) const [pendingFile, setPendingFile] = useState(null) const [previewScript, setPreviewScript] = useState(null) const [showUploadDialog, setShowUploadDialog] = useState(false) - const endpointType = useMemo(() => (tab === 'lua' ? 'lua' : 'shader'), [tab]) - const fetchAssets = async () => { if (!apiUrl) return setLoading(true) try { - const res = await fetch(`${apiUrl}/api/custom-assets/${endpointType}`) + const res = await fetch(`${apiUrl}/api/custom-assets/shader`) const data = await res.json() setAssets(Array.isArray(data) ? data : []) - if (tab === 'lua' && Array.isArray(data) && data.length > 0 && !selectedLua) { - setSelectedLua(data[0].filename) - } - if (tab === 'lua' && Array.isArray(data) && data.length === 0) { - setSelectedLua(null) - } - if (tab === 'shader' && Array.isArray(data) && data.length > 0 && !selectedShader) { + if (Array.isArray(data) && data.length > 0 && !selectedShader) { setSelectedShader(data[0].filename) } - if (tab === 'shader' && Array.isArray(data) && data.length === 0) { + if (Array.isArray(data) && data.length === 0) { setSelectedShader(null) } } finally { @@ -57,7 +44,7 @@ export default function AssetManager() { useEffect(() => { fetchAssets() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [apiUrl, endpointType]) + }, [apiUrl]) const onUploadClick = () => { fileInputRef.current?.click() @@ -66,7 +53,7 @@ export default function AssetManager() { const onFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file || !apiUrl) return - + // Store the selected file and show a preview dialog. Only upload after confirmation. setPendingFile(file) try { @@ -93,7 +80,7 @@ export default function AssetManager() { if (!pendingFile || !apiUrl) return const formData = new FormData() formData.append('file', pendingFile) - await fetch(`${apiUrl}/api/custom-assets/${endpointType}`, { + await fetch(`${apiUrl}/api/custom-assets/shader`, { method: 'POST', body: formData, }) @@ -105,13 +92,10 @@ export default function AssetManager() { const onDelete = async (filename: string) => { if (!apiUrl) return if (!window.confirm(`Delete ${filename}?`)) return - await fetch(`${apiUrl}/api/custom-assets/${endpointType}/${encodeURIComponent(filename)}`, { + await fetch(`${apiUrl}/api/custom-assets/shader/${encodeURIComponent(filename)}`, { method: 'DELETE', }) - if (tab === 'lua' && selectedLua === filename) { - setSelectedLua(null) - } - if (tab === 'shader' && selectedShader === filename) { + if (selectedShader === filename) { setSelectedShader(null) } await fetchAssets() @@ -121,12 +105,7 @@ export default function AssetManager() {

Asset Manager

-

Upload, export and delete custom Lua scripts and Shadertoy shaders.

-
- -
- - +

Upload, export and delete custom Shadertoy shaders.

@@ -134,19 +113,19 @@ export default function AssetManager() { ref={fileInputRef} type="file" className="hidden" - accept={tab === 'lua' ? '.lua' : '.frag'} + accept={'.frag'} onChange={onFileChange} />
-

{tab === 'lua' ? 'Lua Scripts' : 'Shaders'} ({assets.length})

+

Shaders ({assets.length})

@@ -165,21 +144,16 @@ export default function AssetManager() { ) : assets.map((asset) => ( { - if (tab === 'lua') setSelectedLua(asset.filename) - else setSelectedShader(asset.filename) - }} + className={`border-b border-border/60 ${selectedShader === asset.filename ? 'bg-muted/60' : '' + }`} + onClick={() => setSelectedShader(asset.filename)} >
{asset.filename} {Math.max(1, Math.round(asset.size / 1024))} KB
- {tab === 'lua' ? ( - - ) : ( - - )} +
@@ -213,11 +183,7 @@ export default function AssetManager() { {pendingFile ? pendingFile.name : 'Preview the script before uploading.'}
- {tab === 'lua' ? ( - - ) : ( - - )} +
diff --git a/src_matrix/server/custom_assets_management.cpp b/src_matrix/server/custom_assets_management.cpp index f43bc208..ecfa96fe 100644 --- a/src_matrix/server/custom_assets_management.cpp +++ b/src_matrix/server/custom_assets_management.cpp @@ -10,95 +10,112 @@ using json = nlohmann::json; namespace fs = std::filesystem; -namespace { -struct AssetTypeConfig { - std::string type; - fs::path directory; - std::string extension; -}; - -std::optional asset_type_from_param(const std::string &type) { - const auto root = get_exec_dir() / "data"; - if (type == "lua") { - return AssetTypeConfig{type, root / "custom_lua", ".lua"}; - } - if (type == "shader" || type == "shaders") { - return AssetTypeConfig{type, root / "custom_shaders", ".frag"}; - } - return std::nullopt; -} - -bool is_safe_filename(const std::string &filename) { - return !filename.empty() && - filename.find('/') == std::string::npos && - filename.find('\\') == std::string::npos && - filename.find("..") == std::string::npos; -} - -std::optional> parse_multipart_file(const std::string &content_type, const std::string &body) { - const auto boundary_pos = content_type.find("boundary="); - if (boundary_pos == std::string::npos) { +namespace +{ + struct AssetTypeConfig + { + std::string type; + fs::path directory; + std::string extension; + }; + + std::optional asset_type_from_param(const std::string &type) + { + const auto root = get_exec_dir() / "data"; + if (type == "shader" || type == "shaders") + { + return AssetTypeConfig{type, root / "custom_shaders", ".frag"}; + } return std::nullopt; } - std::string boundary = content_type.substr(boundary_pos + 9); - if (!boundary.empty() && boundary.front() == '"') { - boundary.erase(0, 1); - } - if (!boundary.empty() && boundary.back() == '"') { - boundary.pop_back(); - } - if (boundary.empty()) { - return std::nullopt; + bool is_safe_filename(const std::string &filename) + { + return !filename.empty() && + filename.find('/') == std::string::npos && + filename.find('\\') == std::string::npos && + filename.find("..") == std::string::npos; } - const std::string boundary_marker = "--" + boundary; - size_t pos = 0; - while (true) { - const auto part_start = body.find(boundary_marker, pos); - if (part_start == std::string::npos) { - break; + std::optional> parse_multipart_file(const std::string &content_type, const std::string &body) + { + const auto boundary_pos = content_type.find("boundary="); + if (boundary_pos == std::string::npos) + { + return std::nullopt; } - auto cursor = part_start + boundary_marker.size(); - if (body.compare(cursor, 2, "--") == 0) { - break; + std::string boundary = content_type.substr(boundary_pos + 9); + if (!boundary.empty() && boundary.front() == '"') + { + boundary.erase(0, 1); } - if (body.compare(cursor, 2, "\r\n") == 0) { - cursor += 2; - } - - const auto headers_end = body.find("\r\n\r\n", cursor); - if (headers_end == std::string::npos) { - break; + if (!boundary.empty() && boundary.back() == '"') + { + boundary.pop_back(); } - const std::string headers = body.substr(cursor, headers_end - cursor); - const auto filename_pos = headers.find("filename=\""); - const auto data_start = headers_end + 4; - const auto data_end = body.find("\r\n" + boundary_marker, data_start); - if (data_end == std::string::npos) { - break; + if (boundary.empty()) + { + return std::nullopt; } - if (filename_pos != std::string::npos) { - const auto filename_start = filename_pos + 10; - const auto filename_end = headers.find('"', filename_start); - if (filename_end != std::string::npos) { - std::string filename = headers.substr(filename_start, filename_end - filename_start); - std::string data = body.substr(data_start, data_end - data_start); - return std::make_pair(filename, data); + const std::string boundary_marker = "--" + boundary; + size_t pos = 0; + while (true) + { + const auto part_start = body.find(boundary_marker, pos); + if (part_start == std::string::npos) + { + break; } + + auto cursor = part_start + boundary_marker.size(); + if (body.compare(cursor, 2, "--") == 0) + { + break; + } + if (body.compare(cursor, 2, "\r\n") == 0) + { + cursor += 2; + } + + const auto headers_end = body.find("\r\n\r\n", cursor); + if (headers_end == std::string::npos) + { + break; + } + const std::string headers = body.substr(cursor, headers_end - cursor); + const auto filename_pos = headers.find("filename=\""); + const auto data_start = headers_end + 4; + const auto data_end = body.find("\r\n" + boundary_marker, data_start); + if (data_end == std::string::npos) + { + break; + } + + if (filename_pos != std::string::npos) + { + const auto filename_start = filename_pos + 10; + const auto filename_end = headers.find('"', filename_start); + if (filename_end != std::string::npos) + { + std::string filename = headers.substr(filename_start, filename_end - filename_start); + std::string data = body.substr(data_start, data_end - data_start); + return std::make_pair(filename, data); + } + } + + pos = data_end + 2; } - pos = data_end + 2; + return std::nullopt; } - - return std::nullopt; -} } -std::unique_ptr Server::add_custom_assets_routes(std::unique_ptr router) { - router->http_get("/api/custom-assets/:type", [](auto req, auto params) { +std::unique_ptr Server::add_custom_assets_routes(std::unique_ptr router) +{ + router->http_get("/api/custom-assets/:type", [](auto req, auto params) + { const auto type = std::string(params["type"]); const auto cfg_opt = asset_type_from_param(type); if (!cfg_opt.has_value()) { @@ -129,10 +146,10 @@ std::unique_ptr Server::add_custom_assets_routes(std::unique_p result.push_back(item); } - return reply_with_json(req, result); - }); + return reply_with_json(req, result); }); - router->http_post("/api/custom-assets/:type", [](auto req, auto params) { + router->http_post("/api/custom-assets/:type", [](auto req, auto params) + { const auto type = std::string(params["type"]); const auto cfg_opt = asset_type_from_param(type); if (!cfg_opt.has_value()) { @@ -172,10 +189,10 @@ std::unique_ptr Server::add_custom_assets_routes(std::unique_p return reply_with_json(req, json{ {"success", true}, {"filename", filename}, - }); - }); + }); }); - router->http_delete("/api/custom-assets/:type/:filename", [](auto req, auto params) { + router->http_delete("/api/custom-assets/:type/:filename", [](auto req, auto params) + { const auto type = std::string(params["type"]); const auto filename = std::string(params["filename"]); const auto cfg_opt = asset_type_from_param(type); @@ -202,10 +219,10 @@ std::unique_ptr Server::add_custom_assets_routes(std::unique_p return reply_with_json(req, json{ {"success", true}, {"filename", filename}, - }); - }); + }); }); - router->http_get("/api/custom-assets/:type/:filename/download", [](auto req, auto params) { + router->http_get("/api/custom-assets/:type/:filename/download", [](auto req, auto params) + { const auto type = std::string(params["type"]); const auto filename = std::string(params["filename"]); const auto cfg_opt = asset_type_from_param(type); @@ -228,9 +245,7 @@ std::unique_ptr Server::add_custom_assets_routes(std::unique_p .append_header(restinio::http_field::content_type, MimeTypes::getType(target_path.string())) .append_header(restinio::http_field::content_disposition, "attachment; filename=\"" + filename + "\""); Server::add_cors_headers(response); - return response.set_body(restinio::sendfile(target_path)).done(); - }); + return response.set_body(restinio::sendfile(target_path)).done(); }); return std::move(router); } - diff --git a/vcpkg.json b/vcpkg.json index 01c24ad6..ddfa68ca 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -59,20 +59,6 @@ "dependencies": [ "shadertoy-headless" ] - }, - "scripted-scenes-matrix": { - "description": "Lua scripted scenes support (ScriptedScenes plugin). Installs lua + sol2 via vcpkg.", - "dependencies": [ - "lua", - "sol2" - ] - }, - "scripted-scenes-desktop": { - "description": "Lua scripted scenes support for desktop (ScriptedScenes plugin). Installs lua + sol2 via vcpkg.", - "dependencies": [ - "lua", - "sol2" - ] } }, "name": "led-matrix",