diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7952bcc..cb39c2b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,8 +9,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - run: cargo check test: @@ -19,8 +17,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - name: Install alsa and udev run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev if: runner.os == 'linux' @@ -33,7 +29,6 @@ jobs: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: - toolchain: stable components: rustfmt - run: cargo fmt --all -- --check @@ -44,6 +39,5 @@ jobs: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: - toolchain: stable components: clippy - run: cargo clippy -- -D warnings diff --git a/.prototools b/.prototools new file mode 100644 index 0000000..36d6482 --- /dev/null +++ b/.prototools @@ -0,0 +1,4 @@ +lefthook = "2.0.2" + +[plugins.tools] +lefthook = "https://raw.githubusercontent.com/ageha734/proto-plugins/refs/heads/master/toml/lefthook.toml" diff --git a/ARCHITECTURE_REVIEW.md b/ARCHITECTURE_REVIEW.md new file mode 100644 index 0000000..c34828b --- /dev/null +++ b/ARCHITECTURE_REVIEW.md @@ -0,0 +1,571 @@ +# `seldom_pixel` Architecture Review & Implementation Plan + +_Last updated: 2025-12-23_ + +This document captures an architectural review of `seldom_pixel` plus a concrete implementation plan, prioritizing easy, high-value improvements. + +--- + +## 1. High-Level Intent + +- Bevy plugin for limited-palette 2D pixel art games. +- Renders into a logical pixel buffer (`ScreenSize`) using 8-bit palette indices, then uploads as an `R8Uint` texture. +- A WGSL shader (`src/screen.wgsl`) maps indices to RGB via a uniform palette. +- Features: + - Sprites (`sprite.rs`) + - Tilemaps (`map.rs`) + - Text / typefaces (`text.rs`) + - Filters (`filter.rs`) + - UI layout (`ui.rs`) + - Particles (`particle.rs`, feature-gated) + - Lines (`line.rs`, feature-gated) + - Cursor and camera (`cursor.rs`, `camera.rs`) + +`PxLayer` (user type, usually via `#[px_layer]`) drives z-ordering and layer semantics across rendering, filters, and picking. + +--- + +## 2. Project Direction (2025-12) + +These are the goals driving the next architectural changes and should guide refactors. + +1. **Decouple frame generation from sprites.** + - Frames should be produced by a separate, composable module. + - Sprites should consume frames, not embed animation logic. +2. **Make animation optional and fully controllable.** + - Animation should be an opt-in system that can be replaced or driven externally. + - Users should be able to control frame selection directly (e.g., manual index changes or linear-algebra time mapping). +3. **Enable pixel-perfect picking at the current frame.** + - Picking must be able to test mouse hits against the actual rendered pixel of the current frame, not just a rectangle. + - The architecture should remain open to supporting future masking or per-pixel collision logic. +4. **Support composite sprites formed from multiple frame parts.** + - Users should be able to assemble sprites from multiple pieces (equipment, layers, effects). + - Composition should work as baked assets, runtime composition, or entity-based assembly. + - Frame selection should be driven by a single master frame with per-part bindings. + +--- + +## 3. Key Architectural Decisions + +### 3.1 Plugin composition + +- `PxPlugin` (`src/lib.rs`) wires together: +- `animation`, `blink`, `camera`, `cursor`, `palette`, `picking`, `position`, + `screen`, `filter`, `line`, `map`, `rect`, `sprite`, `text`, `ui`. +- Each module exposes a `plug` function; they are domain-focused but tightly integrated. + +### 3.2 Headed vs headless + +- `headed` feature gates window, render graph, picking, and input integration. +- Core components / resources exist without `headed`, so logic can run headless. + +### 3.3 CPU-side renderer + +- `src/screen.rs`’s `PxRenderNode`: + - Queries render-world ECS for maps, tiles, sprites, texts, rects, lines, filters. + - Groups them by layer into a `BTreeMap`. + - Draws each layer into a CPU `PxImage` (palette indices) and then uploads that to a GPU `R8Uint` texture. + - A full-screen quad (`screen.wgsl`) turns indices into colors via a 256-entry palette uniform. + +### 3.4 Palette-centric asset pipeline + +- `Palette` asset (`palette.rs`) describes the allowed colors; top-left pixel is background, colors are packed from the rest of the image. +- A global static palette (`ASSET_PALETTE`) is initialized once at startup. +- Asset loaders for sprites (`sprite.rs`), tilesets (`map.rs`), filters (`filter.rs`), typefaces (`text.rs`) call `asset_palette().await` and convert RGBA textures into `PxImage` palette indices at load time. + +### 3.5 Animation abstraction + +- `Frames` trait (`animation.rs`) describes “things with frames backed by a `PxImage`”. +- `AnimatedAssetComponent` ties ECS components to assets and exposes `max_frame_count`. +- `PxAnimation`, `PxFrameSelector`, `PxFrameTransition`: + - Control which frame is used (by index or normalized progress). + - Optional ordered dithering to blend between frames. +- `draw_frame` and `draw_spatial` centralize “draw this animated asset into a `PxImageSliceMut` at a position/anchor/canvas, with filters”. +- Today, animation is effectively embedded in sprites via these traits and component bindings, which makes it harder to compose or override animation behavior from outside. + +### 3.6 World vs camera space + +- `PxCanvas` (`camera.rs`) distinguishes: + - `World`: positions relative to world, offset by `PxCamera`. + - `Camera`: positions relative to the camera (HUD/UI). +- `draw_spatial` applies `PxCamera` for world objects and keeps camera-space sprites fixed. + +### 3.7 UI as layout over pixel primitives + +- `ui.rs` defines layout structures: + - Containers: `PxRow`, `PxGrid`, `PxStack`, `PxScroll`. + - Decoration: `PxMinSize`, `PxMargin`. + - Root marker: `PxUiRoot`. +- `calc_min_size` and `layout_inner` compute per-frame layout in pixel space against `Screen::computed_size`. +- UI is built from the same primitives as the renderer: `PxRect`, `PxText`, `PxSprite`, `PxFilterLayers`. + +### 3.8 Filters as palette-to-palette maps + +- `PxFilterAsset` wraps a `PxImage` mapping `[palette_index, frame] -> palette_index`. +- `PxFilter` attaches a filter to a single entity. +- `PxFilterLayers` lets a filter apply to: + - A single layer (`Single { layer, clip }`) + - A range of layers (`Range(RangeInclusive)`) + - A list of layers (`Many(Vec)`) +- `clip` vs `over` distinguishes “pre-layer only entity pixels” vs “post-layer including background”. + +### 3.9 Picking integration + +- `picking.rs` integrates with `bevy_picking`: + - Uses `PxRect` + `PxFilterLayers` as hit regions. + - Computes per-layer depths with a `BTreeMap` rather than a fixed z. +- Current picking is rectangle-based and does not inspect actual sprite pixels or current animation frames. + +--- + +## 4. Potential Problems / Code Smells + +### 4.1 `init_screen` never marks itself initialized + +**Location:** `src/screen.rs` + +```rust +fn init_screen( + mut initialized: Local, + // ... +) { + if *initialized { + return; + } + + let Some(palette) = palettes.get(&**palette) else { + return; + }; + + // set screen.palette ... + + *initialized = false; +} +``` + +- `initialized` starts as `false`; once a palette is present, this system keeps running every frame. +- Intended behavior appears to be “run once on successful palette load”, but we never set it to `true`. +- Results: unnecessary palette copy per frame and redundant overlap with `update_screen_palette`. + +### 4.2 `PxImage::trim_right` can shrink width to zero + +**Location:** `src/image.rs` + +- `trim_right` trims trailing all-zero columns by: + - Testing the last column for zero. + - Removing it from each row. + - Decrementing `self.width`. +- If the entire image is all zero, `self.width` can reach `0` while the loop still calls `self.height()` (`len / width`), causing division by zero. +- Likely only triggered by fully transparent/misconfigured assets, but still a correctness bug. + +### 4.3 Palette size > 255 not enforced + +**Location:** `src/palette.rs` (`Palette::new`) + +- Assumes “up to 255 colors” (index 0 is special, rest are color entries). +- `indices` map uses `i as u8` for color index, without checking how many colors are in the palette. +- More than 255 colors overflows silently and wraps indices modulo 256 → very confusing visual results. + +### 4.4 `Palette::new` uses `unwrap` inside loader + +**Location:** `src/palette.rs` + +- `image.convert(TextureFormat::Rgba8UnormSrgb).unwrap()` will panic if conversion fails. +- The loader already returns `Result`; this should be reported as an error instead. + +### 4.5 Global static `ASSET_PALETTE` + +**Location:** `src/palette.rs` + +- Uses `static mut ASSET_PALETTE`, `AtomicBool`, and `Event` to coordinate palette initialization. +- The current code is carefully guarded, but the pattern: + - Is non-idiomatic in Bevy (compared to resource-based approaches). + - Is easy to get wrong if extended (unsafe invariants). + +### 4.6 `PxImageSliceMut::for_each_mut` allocates on every call + +**Location:** `src/image.rs` + +```rust +self.image.iter_mut().enumerate().collect::>()[..] + .iter_mut() + .for_each(|(i, row)| { + row.iter_mut().enumerate().collect::>()[..] + .iter_mut() + .for_each(|(j, pixel)| { + f(slice_idx, image_idx, pixel); + }); + }); +``` + +- Each call allocates: + - A `Vec` of rows with their indices. + - For each row, another `Vec` of columns and pixels. +- This is a hot path used to draw sprites, tilemaps, text, filters, etc. The allocations are unnecessary and expensive. + +### 4.7 `PxRect` and `PxLine` draw over entire images + +**Locations:** `src/rect.rs`, `src/line.rs` + +- `PxRect`’s `draw` loops over `0..image.image_width()` × `0..image.image_height()`, then checks `contains_pixel` and `invert` to decide whether to apply the filter. +- `PxLine`: + - Builds a `HashSet` of all Bresenham positions. + - Loops over the entire image, checking membership. +- These approaches scale poorly with screen size and number of rects/lines. + +### 4.8 Minor API / ergonomics issues + +- `update_screen_palette` takes two `PaletteHandle` resources (`palette_handle`, `palette`) and uses them differently: confusing naming. +- `update_key_fields` (`ui.rs`) uses a slightly odd pattern to pick a key event; not wrong, but non-obvious. +- `PxTiles::pos` depends on `tile_poses` map, which can retain stale entries if tiles are despawned directly. + +--- + +## 5. Easy Wins: Correctness & Performance + +These are low-risk, localized changes that are good starting points. + +### 5.1 Fix `init_screen` initialization logic + +**Change:** + +- In `init_screen`, set `*initialized = true` after successfully updating `screen.palette`. +- Keep `update_screen_palette` as the mechanism for responding to palette changes. + +**Benefits:** + +- Avoids unnecessary palette copying every frame. +- Clarifies the intended “run once on initialization” semantics. + +--- + +### 5.2 Harden `PxImage::trim_right` + +**Change:** + +- Add guards to ensure: + - If `self.image.is_empty()` or `self.width == 0`, return immediately. + - If trimming would make `self.width` become `0`, stop trimming. +- Optionally add a small unit test (once a test harness exists) for: + - Fully transparent images. + - Images with a non-empty last column. + +**Benefits:** + +- Eliminates a potential division-by-zero crash on malformed assets. +- Makes trimming behavior explicit and safe. + +--- + +### 5.3 Enforce palette size limit and avoid `unwrap` + +**Changes:** + +1. In `Palette::new`: + - Replace `.convert(...).unwrap()` with an error mapping, e.g.: + + ```rust + let image = image + .convert(TextureFormat::Rgba8UnormSrgb) + .ok_or("could not convert palette image to Rgba8UnormSrgb")?; + ``` + +2. After building `colors`, enforce `colors.len() <= 256`: + - If exceeded, return an error like `"palette contains more than 255 colors (max 255)"`. + +**Benefits:** + +- Avoids panics in the asset loader. +- Gives clear feedback when a palette image is misconfigured. + +--- + +### 5.4 Implement `PxImageSliceMut::for_each_mut` without allocations + +**Change (conceptual):** + +- Re-implement `for_each_mut` as: + - Precompute: + - `row_min`, `row_max` from `slice.min.y` / `slice.max.y`. + - `col_min`, `col_max` from `slice.min.x` / `slice.max.x`. + - Nested loops: + + ```rust + let slice_width = self.slice.width(); + for (row_index, row) in self.image[row_min..row_max].iter_mut().enumerate() { + let y = row_index + row_min; + for x in col_min..col_max { + let slice_i = + (y - self.slice.min.y) * slice_width + (x - self.slice.min.x); + let image_i = y * self.width + x; + let pixel = &mut row[x]; + f(slice_i as usize, image_i as usize, pixel); + } + } + ``` + +- No `Vec` allocations; everything is index arithmetic. + +**Benefits:** + +- Removes per-call allocations from hot paths. +- Likely a noticeable improvement at higher resolutions or entity counts. + +--- + +### 5.5 Restrict `PxRect` and `PxLine` drawing to affected regions + +**Rect (`PxRect` in `src/rect.rs`):** + +- Compute rect bounds in image space once (using the existing position/anchor and slice offsets). +- Intersect the rect with slice bounds, and iterate only over that intersection, applying filter to image pixels inside / outside as per `invert`. + +**Line (`PxLine` in `src/line.rs`):** + +- Instead of: + - Building a `HashSet` of all Bresenham points, then + - Scanning the full image, +- Use only the Bresenham output: + - For each point, map to image coordinates. + - Check whether it lies in the slice; if so, apply filter to that pixel. + - For `invert`, you may still need some complementary logic, but you can avoid scanning the entire image. + +**Benefits:** + +- Complexity becomes proportional to object size, not full screen size. +- Particularly important for larger logical screens or many UI elements. + +--- + +## 6. Baseline Architectural Improvements + +These are more structural and can be tackled after (or alongside) the easy wins. + +### 6.1 Modularize `screen.rs` and `ui.rs` + +**Goal:** Reduce file size and clarify responsibilities. + +**Directions:** + +- `screen`: + - `screen/pipeline.rs` – render graph, `PxPipeline`, `PxUniform`, `PxUniformBuffer`, node registration. + - `screen/node.rs` – `PxRenderNode`, queries, layer aggregation. + - `screen/draw.rs` – per-type drawing (maps, sprites, text, rects, lines, filters, cursor). +- `ui`: + - `ui/layout.rs` – `calc_min_size`, `layout_inner`, `layout`. + - `ui/input.rs` – `PxKeyField`, `PxTextField`, and their update systems. + - `ui/widgets.rs` – `PxRow`, `PxGrid`, `PxStack`, `PxScroll`, etc. + +**Benefits:** + +- Easier navigation and reasoning. +- Clearer extension points for new drawables and UI patterns. + +--- + +### 6.2 Clarify palette lifecycle and ownership + +**Goal:** Reduce reliance on globals and make palette use explicit. + +**Potential future refactor:** + +- Store “asset palette” as a normal resource and/or asset, referenced by handle. +- Pass palette information into loaders via settings or context rather than `asset_palette()` global. +- Keep `Screen::palette` as the runtime palette used by the shader, with `update_screen_palette` as the only mutator. + +**Benefits:** + +- Safer and easier to test. +- Opens the door for multiple palettes, palette blending, etc. + +--- + +### 6.3 Sharpen layering / filtering semantics + +**Goal:** Make it easier to understand and reuse `PxFilterLayers`. + +**Ideas:** + +- Provide helper constructors for common patterns: + - “Filter exactly this layer, clipped”. + - “Overlay filter over these layers, including background”. + - “Filter all layers in a UI group”. +- Consider separating “visual filter layers” from “picking layers” if they diverge further. + +**Benefits:** + +- Reduces boilerplate and potential mistakes in specifying layer ranges. +- Makes behavior more discoverable. + +--- + +## 7. Longer-Term Performance Directions + +### 7.1 Reuse CPU and GPU buffers + +**Idea:** + +- Introduce a `PxRenderBuffer` resource containing: + - CPU `PxImage` backing storage sized to `Screen::computed_size`. + - GPU `Texture` reused across frames. +- On screen resize: + - Reallocate both buffers. +- In `PxRenderNode`: + - Clear the existing buffer each frame. + - Upload new data into the existing texture (no `create_texture` per frame). + +**Benefit:** + +- Less allocation / texture churn at high frame rates. + +### 7.2 Explore GPU-centric palette rendering + +**Idea:** + +- Keep assets palette-indexed but: + - Upload sprites/tiles/typefaces as `R8Uint` textures. + - Draw them directly into a render target using instancing / batching, with palette lookup and filters in shaders. +- Use CPU compositing only when necessary (advanced spatial filters, special effects). + +**Benefit:** + +- Better scalability for large scenes and resolutions. +- Retains palette-driven art style while leveraging GPU strengths. + +--- + +## 8. Directional Architecture Changes (Aligned with Project Goals) + +These changes align directly with the 2025-12 project goals and should be considered alongside the existing roadmap. + +### 8.1 Frame generation as a standalone module + +**Idea:** + +- Introduce a small set of reusable components and traits: + - `PxFrameSource` (asset or runtime source of frames). + - `PxFrameView` (current frame index + optional metadata like size or anchor). + - `PxFrameControl` (explicit or computed frame selection). +- Sprites and other drawables should depend only on `PxFrameView`, not on animation traits. + +**Benefit:** + +- Frames become composable; animation logic can be swapped or driven externally. + +### 8.2 Animation as a separate, optional system + +**Idea:** + +- Move animation into a module that updates `PxFrameControl` based on: + - Manual steps (events, state machines). + - Time-based functions (linear mapping, easing, or custom math). + - Deterministic stepping (fixed tick rate independent of frame rate). +- The animation system should not be required for rendering. + +**Benefit:** + +- Users gain precise control of frame progression and can opt out entirely. + +### 8.3 Pixel-perfect picking for current frame + +**Idea:** + +- Extend picking to support: + - A `PxHitMask` source (derived from the current `PxFrameView`). + - Querying a pixel at cursor position to test transparency or a specific index. +- Keep the existing rectangle picking for coarse/fast checks, but allow pixel-perfect refinement when needed. + +**Benefit:** + +- Enables precise mouse interaction and future collision logic based on pixels. + +--- + +### 8.4 Composite sprites from multiple frame parts + +**Idea:** + +- Treat composite sprites as another `Frames + Spatial` source. +- Introduce a small frame-binding layer so parts derive their frame from a master: + - `Inherit` (normalized master progress). + - `Map(Vec)` (explicit per-part frame mapping). + - Optional `Offset`/`Scale` (simple timing adjustments). +- Support three implementations behind the same interface: + 1. **Baked composite asset** (`PxCompositeSpriteAsset`) that compiles parts into a single sheet. + 2. **Runtime composite** (`PxCompositeSprite`) that draws parts into a shared slice each frame. + 3. **Entity-per-part** helpers that propagate the master frame to children. + +**Benefit:** + +- One animation source controls many visual parts. +- Composition is flexible without duplicating renderer logic. +- Baked assets preserve the GPU path, while runtime composition enables customization. + +--- + +## 9. Implementation Strategy (Prioritized) + +### Phase 1 – Quick, Safe Wins (Completed) + +1. **Fix `init_screen` flag** (done) + - Set `*initialized = true` after palette is first applied. + - Confirm palette still updates via `update_screen_palette`. + +2. **Harden `PxImage::trim_right`** (done) + - Add width/empty checks. + - Ensure no division-by-zero when trimming fully transparent images. + +3. **Improve `Palette::new` robustness** (done) + - Replace `unwrap` on `convert`. + - Enforce `<= 255` colors and emit a clear error otherwise. + +4. **Optimize `PxImageSliceMut::for_each_mut`** (done) + - Remove all `Vec` allocations; use index arithmetic loops. + +5. **Constrain `PxRect` drawing** (done) + - Limit iteration to intersecting region of rect and slice. + +6. **Constrain `PxLine` drawing** (done) + - Draw only along Bresenham path (plus any required inverted region handling) without scanning full image. + +### Phase 2 – Structural Clean-up (Completed) + +7. **Refactor `screen.rs` into submodules** (done) + - Keep public API stable; this is internal re-organization. + +8. **Refactor `ui.rs` into layout/input/widgets modules** (done) + - Simplify and clarify key/text field flows. + +### Phase 3 – Palette & Layers + +9. **Redesign palette lifecycle** (done) + - Replace global `ASSET_PALETTE` with resource-centric approach. + - Adjust loaders to use explicit palette context. + +10. **Enhance `PxFilterLayers` ergonomics** (done) + - Add helpers for common patterns and clearer docs. + +### Phase 4 – Advanced Optimization + +11. **Add `PxRenderBuffer` for buffer reuse** (done) + - Reuse CPU/GPU buffers across frames. + +12. **Prototype GPU-centric palette pipeline** (prototype done: sprites only) + - Add `gpu_palette` feature flag and GPU sprite pass (palette lookup in shader). + - Opt-in via `PxGpuSprite`; CPU renderer skips those sprites. + - Current limits: sprites only, no filters, no dithering, drawn after CPU pass. + +### Phase 5 – Frame / Animation / Picking Goals + +13. **Introduce frame-view components** (done) + - Added `PxFrameView` and `PxFrameControl` to decouple frame selection from sprites. + +14. **Extract animation to a separate module** (done) + - Frame selection and drawing moved to `frame` module; `PxAnimationPlugin` is opt-in. + +15. **Add pixel-perfect picking** (done) + - `PxPixelPick` enables per-pixel picking for sprites using current frame data. + +--- + +This plan starts with the smallest, safest changes (Phase 1), which you can implement and ship incrementally, and then moves toward deeper refactors and optimizations as needed. diff --git a/CHANGELOG.md b/CHANGELOG.md index b81a4dd..167d072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,51 @@ # Changelog +## 0.9.0-dev (Unreleased) + +### Added + +- Frame view/control API: `PxFrameView`, `PxFrameControl`, `PxFrameSelector`, `PxFrameTransition`, and + `PxFrame` alias for manual or externally-driven frame selection. +- `PxAnimationPlugin` so animation systems are opt-in. +- Pixel-perfect sprite picking with `PxPixelPick` (uses the current frame, including dithering). +- `gpu_palette` feature with `PxGpuSprite` and a GPU sprite pass (sprites only; filters and dithering + are not supported yet). See `ARCHITECTURE_REVIEW.md` for details. +- New examples: `pixel_pick` and `gpu_palette`. + +### Changed + +- Animation systems are no longer part of `PxPlugin`; add `PxAnimationPlugin` explicitly. +- Rendering now reuses screen buffers across frames to reduce allocations. +- `PxFilterLayers` gained helper constructors for common layer/filter patterns. + +### Fixed + +- `PxImage::trim_right` no longer risks shrinking to zero width. +- Palette loading now reports conversion errors and rejects palettes with more than 255 colors. + +### Performance + +- Eliminated per-call allocations in `PxImageSliceMut::for_each_mut`. +- `PxRect` and `PxLine` drawing now iterates only over affected pixels instead of the full screen. + ## 0.8 (2025-01-01) ### Added - `PxSprite` and `PxFilter` components to be used instead of `Handle` (which -were called `Handle`) + were called `Handle`) - `PxAnimation` component - `PxMap` component, which contains `PxTiles`, which was previously called `PxMap` and is no longer -a component + a component - `ScreenSize` enum, with a variant that allows dynamically changing draw resolution as the window's -aspect ratio changes. `PxPlugin` now accepts an `impl Into` for the screen size (which -may be a `UVec2`, like before). + aspect ratio changes. `PxPlugin` now accepts an `impl Into` for the screen size (which + may be a `UVec2`, like before). - `PaletteHandle` resources, which contains the current `Handle` - `SelectLayerFn` trait, which layer selection functions must now implement. It has the additional -bound of `Clone`. + bound of `Clone`. - `PaletteLoader` (`.px_palette.png`), `PxSpriteLoader` (`.px_sprite.png`), `PxFilterLoader` -(`.px_filter.png`), `PxTypefaceLoader` (`.px_typeface.png`) and `PxTilesetLoader` -(`.px_tileset.png`) + (`.px_filter.png`), `PxTypefaceLoader` (`.px_typeface.png`) and `PxTilesetLoader` + (`.px_tileset.png`) - `PxButtonSprite` and `PxButtonFilter` components - `Orthogonal` and `Diagonal` math types @@ -25,16 +53,16 @@ bound of `Clone`. - Updated `bevy` to 0.15 - `seldom_pixel` entities are extracted to the render world and drawn there. Involved components -implement `ExtractComponent` and involved resources implement `ExtractResource`. Due to this change, -entities on the same layer Z-fight. This behavior may change in the future. + implement `ExtractComponent` and involved resources implement `ExtractResource`. Due to this change, + entities on the same layer Z-fight. This behavior may change in the future. - `PxSpriteData`, `PxFilter`, and `PxTilesetData` are now called `PxSpriteAsset`, `PxFilterAsset`, -and `PxTileset` respectively + and `PxTileset` respectively - `PxAnimationDirection`, `PxAnimationDuration`, `PxAnimationFinishBehavior`, and -`PxAnimationFrameTransition` are no longer components and are instead fields of the new -`PxAnimation` component + `PxAnimationFrameTransition` are no longer components and are instead fields of the new + `PxAnimation` component - `PxText` has a `Handle` component, replacing the handle's use as a component - `PxEmitterFrequency` and `PxEmitterSimulation` are no longer components and are instead fields of -the new `PxEmitter` component + the new `PxEmitter` component - `Palette` is an asset instead of a resource - `#[px_layer]` derives `ExtractComponent` - `PxAnimationFinished`, `PxHover`, and `PxClick` are table components. They were sparse set. @@ -42,12 +70,12 @@ the new `PxEmitter` component ### Removed - The built-in asset management (`PxAsset`, `PxAssets`, `PxAssetTrait`, `PxAssetData`, and -`LoadingAssets`) in favor of the new asset loaders. + `LoadingAssets`) in favor of the new asset loaders. - Bundles (`PxSpriteBundle`, `PxFilterBundle`, `PxAnimationBundle`, `PxTextBundle`, `PxMapBundle`, -`PxTileBundle`, `PxEmitterBundle`, `PxButtonSpriteBundle`, and `PxButtonFilterBundle`) in favor of -required components + `PxTileBundle`, `PxEmitterBundle`, `PxButtonSpriteBundle`, and `PxButtonFilterBundle`) in favor of + required components - `PxEmitterSprites`, `PxEmitterRange`, and `PxEmitterFn` in favor of `Vec>`, -`IRect`, and `Box` fields in `PxEmitter` + `IRect`, and `Box` fields in `PxEmitter` - `PxIdleSprite`, `PxHoverSprite`, and `PxClickSprite` in favor of `PxButtonSprite` - `PxIdleFilter`, `PxHoverFilter`, and `PxClickFilter` in favor of `PxButtonFilter` - `PxAnimationStart` in favor of an `Instant` field in `PxAnimation` diff --git a/Cargo.toml b/Cargo.toml index c2a1c2d..2078506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ headed = [ "bevy_input_focus", ] default = ["headed"] +gpu_palette = ["headed"] line = ["line_drawing"] nav = ["seldom_map_nav"] particle = ["bevy_turborand"] @@ -32,6 +33,7 @@ serde = "1.0" line_drawing = { version = "1.0", optional = true } bevy_utils = { version = "0.17.1", default-features = false } bevy_math = { version = "0.17.1", default-features = false } +bevy_mesh = { version = "0.17.1", default-features = false } bevy_transform = { version = "0.17.1", default-features = false } bevy_color = { version = "0.17.1", default-features = false } bevy_platform = { version = "0.17.1", default-features = false } @@ -51,21 +53,31 @@ bevy_shader = { version = "0.17.1", default-features = false, optional = true } bevy_core_pipeline = { version = "0.17.1", default-features = false, optional = true } bevy_picking = { version = "0.17.1", default-features = false, optional = true } bevy_input_focus = { version = "0.17.1", default-features = false, optional = true } -bevy_turborand = { version = "0.11.0", optional = true } -seldom_map_nav = { version = "0.9.0", optional = true } +bytemuck = { version = "1.16.2", features = ["derive"] } +bevy_turborand = { version = "0.12.0", optional = true } +seldom_map_nav = { version = "0.10.0", optional = true } seldom_pixel_macros = { version = "0.3.0-dev", path = "macros" } -seldom_state = { version = "0.14.0", optional = true } +seldom_state = { version = "0.15.0", optional = true } [dev-dependencies] bevy = "0.17.1" -leafwing-input-manager = "0.17.0" +leafwing-input-manager = "0.18.0" rand = "0.8.5" -seldom_state = { version = "0.14.0", features = ["leafwing_input"] } +insta = "1.39.0" +seldom_state = { version = "0.15.0", features = ["leafwing_input"] } [[example]] name = "line" required-features = ["line"] +[[example]] +name = "gpu_palette" +required-features = ["gpu_palette"] + +[[example]] +name = "composite_gpu" +required-features = ["gpu_palette"] + [[example]] name = "particles" required-features = ["particle"] diff --git a/GOALS.md b/GOALS.md new file mode 100644 index 0000000..9316b01 --- /dev/null +++ b/GOALS.md @@ -0,0 +1,28 @@ +# seldom_pixel Goals + +Last updated: 2025-12-22 + +## Product Goals + +- Make frame generation a standalone, composable concern. +- Keep animation optional and fully controllable (manual or math-driven). +- Support pixel-perfect picking against the current frame. +- Support composite sprites composed from multiple frame parts (baked, runtime, or entity-based). + +## Design Goals + +- Separate data (frames) from behavior (animation) cleanly. +- Keep rendering predictable: drawables consume a "current frame" view. +- Preserve headless capability and maintain palette-indexed assets. + +## Non-Goals (for now) + +- A fully GPU-only rendering path. +- Overhauling the public API in a breaking way. +- Removing the existing rectangle-based picking. + +## Open Questions + +- Should pixel-perfect picking use raw frame data, post-filter data, or both? +- How should frame selection be expressed: component, resource, or trait object? +- What is the simplest public API that still allows manual control? diff --git a/README.md b/README.md index eba9d91..673891f 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,11 @@ seldom_pixel = "*" Then add `PxPlugin` to your app. Check out the examples for further usage. +### GPU palette path (experimental) + +Enable the `gpu_palette` feature and add `PxGpuSprite` or `PxGpuComposite` to entities you want +rendered via the GPU palette pass. Filters and dithering are not supported on this path yet. + ## Compatibility | Bevy | `seldom_state` | `seldom_map_nav` | `seldom_interop` | `bevy_ecs_tilemap` | `seldom_pixel` | diff --git a/examples/animated_filters.rs b/examples/animated_filters.rs index f76627a..d36bbb9 100644 --- a/examples/animated_filters.rs +++ b/examples/animated_filters.rs @@ -14,6 +14,7 @@ fn main() { ..default() }), PxPlugin::::new(UVec2::new(51, 35), "palette/palette_1.palette.png"), + PxAnimationPlugin, )) .insert_resource(ClearColor(Color::BLACK)) .add_systems(Startup, init) diff --git a/examples/animated_sprites.rs b/examples/animated_sprites.rs index edf4a08..b76a812 100644 --- a/examples/animated_sprites.rs +++ b/examples/animated_sprites.rs @@ -14,6 +14,7 @@ fn main() { ..default() }), PxPlugin::::new(UVec2::new(51, 35), "palette/palette_1.palette.png"), + PxAnimationPlugin, )) .insert_resource(ClearColor(Color::BLACK)) .add_systems(Startup, init) diff --git a/examples/animated_text.rs b/examples/animated_text.rs index 2dd3d21..ee11d60 100644 --- a/examples/animated_text.rs +++ b/examples/animated_text.rs @@ -14,6 +14,7 @@ fn main() { ..default() }), PxPlugin::::new(UVec2::splat(64), "palette/palette_1.palette.png"), + PxAnimationPlugin, )) .insert_resource(ClearColor(Color::BLACK)) .add_systems(Startup, init) diff --git a/examples/animated_tilemap.rs b/examples/animated_tilemap.rs index 31fed87..83eb85d 100644 --- a/examples/animated_tilemap.rs +++ b/examples/animated_tilemap.rs @@ -15,6 +15,7 @@ fn main() { ..default() }), PxPlugin::::new(UVec2::splat(16), "palette/palette_1.palette.png"), + PxAnimationPlugin, )) .insert_resource(ClearColor(Color::BLACK)) .add_systems(Startup, init) diff --git a/examples/composite_animation.rs b/examples/composite_animation.rs new file mode 100644 index 0000000..1284fbf --- /dev/null +++ b/examples/composite_animation.rs @@ -0,0 +1,67 @@ +// In this program, a composed sprite is animated by driving a master frame manually. + +use bevy::prelude::*; +use seldom_pixel::prelude::*; + +#[derive(Component)] +struct CompositeAnimator; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + resolution: UVec2::splat(512).into(), + ..default() + }), + ..default() + }), + PxPlugin::::new(UVec2::splat(16), "palette/palette_1.palette.png"), + )) + .insert_resource(ClearColor(Color::BLACK)) + .add_systems(Startup, init) + .add_systems(Update, animate_composite) + .run(); +} + +fn init(assets: Res, mut commands: Commands) { + commands.spawn(Camera2d); + + let runner = assets.load("sprite/runner.px_sprite.png"); + + let composite = PxCompositeSprite::new(vec![ + PxCompositePart { + sprite: runner.clone(), + offset: IVec2::ZERO, + frame: PxFrameBinding::default(), + filter: None, + }, + PxCompositePart { + sprite: runner, + offset: IVec2::new(8, 0), + frame: PxFrameBinding::default(), + filter: None, + }, + ]); + + commands.spawn(( + composite, + PxPosition(IVec2::splat(8)), + PxFrameControl::from(PxFrameSelector::Normalized(0.)), + CompositeAnimator, + )); +} + +fn animate_composite( + time: Res