Implement 20 FPS render target with dirty checking optimization #308

Closed
opened 2026-03-16 01:59:27 +00:00 by freemo · 0 comments
Owner

Metadata

Key Value
Branch feature/m2-render-fps
Commit Message feat: implement 20 FPS render target with dirty checking optimization
Parent Epic #306 — Render Pipeline

Background and Context

A MUD's terminal UI doesn't need 60 FPS — text updates at 20 FPS (50ms per frame) feel responsive while keeping CPU usage low. The render loop must maintain this target consistently, compensating for variable processing times. Dirty checking ensures that only windows with actual content changes are re-rendered, avoiding unnecessary ncurses operations.

Expected Behavior

The RenderLoop runs on a dedicated thread at 20 FPS. Each frame:

  1. Record frame start time.
  2. Drain the render queue.
  3. Process ops through the render pipeline.
  4. Calculate elapsed time and sleep for the remainder of the 50ms budget.

Dirty checking is implemented per window:

  • Each window maintains a content hash of its current displayed state.
  • After formatting, the new content hash is compared to the stored hash.
  • If unchanged, the window is skipped in the Ncurses and Composite stages.
  • On change, the window is marked dirty, rendered, and its hash updated.
loop = RenderLoop.new(render_queue, pipeline, fps: 20)
loop.start  # Runs on dedicated thread
loop.stop   # Graceful shutdown

Acceptance Criteria

  • RenderLoop maintains 20 FPS (50ms per frame) target.
  • Frame time compensation: if processing takes 30ms, sleep 20ms.
  • If processing exceeds 50ms, the next frame starts immediately (no negative sleep).
  • Each window tracks a content hash for dirty checking.
  • Unchanged windows are skipped during rendering.
  • Changed windows are re-rendered and their hash updated.
  • RenderLoop#start launches a dedicated render thread.
  • RenderLoop#stop gracefully shuts down the render thread.
  • FPS is configurable (default 20).

Subtasks

Code

  • Create RenderLoop class with #start and #stop lifecycle methods.
  • Implement frame timing with 50ms target and sleep compensation.
  • Implement frame overrun handling (skip sleep, proceed immediately).
  • Implement per-window content hashing for dirty checking.
  • Integrate dirty check into the DirtyCheckStage of the pipeline.
  • Add configurable FPS parameter (default 20).
  • Implement graceful shutdown with thread join.

Quality

  • Docs: Update YARD comments on affected classes and methods. Update relevant Docusaurus documentation pages if applicable.
  • Tests (Cucumber): Add tests/unit/render_fps.feature covering consistent 20 FPS timing, frame overrun handling, dirty window re-rendering, clean window skipping, configurable FPS, start/stop lifecycle, content hash comparison accuracy.
  • Tests (Cucumber Integration): Add integration feature in tests/integration/ for 20 FPS render loop with dirty checking optimization.
  • Tests (Profiling): Run bundle exec rake unit_profile and verify no performance regressions.
  • Quality: Verify coverage >=97% via bundle exec rake unit. If coverage is <97% then review the current unit test coverage report at build/tests/unit/coverage/ and use it to write new Cucumber based unit tests to improve coverage. Specifically, write Cucumber/Gherkin style unit tests that are descriptively named and specifically improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun bundle exec rake unit to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
  • Quality: Run bundle exec rake (default task: unit tests with coverage) and bundle exec rake integration, fix any errors if needed ensuring both pass across entire code base, do not ignore any failure even if it seems unrelated to this commit, fix it.

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation.
  • The commit is pushed to the remote on the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.
## Metadata | Key | Value | |-----|-------| | **Branch** | `feature/m2-render-fps` | | **Commit Message** | `feat: implement 20 FPS render target with dirty checking optimization` | | **Parent Epic** | #306 — Render Pipeline | ## Background and Context A MUD's terminal UI doesn't need 60 FPS — text updates at 20 FPS (50ms per frame) feel responsive while keeping CPU usage low. The render loop must maintain this target consistently, compensating for variable processing times. Dirty checking ensures that only windows with actual content changes are re-rendered, avoiding unnecessary ncurses operations. ## Expected Behavior The `RenderLoop` runs on a dedicated thread at 20 FPS. Each frame: 1. Record frame start time. 2. Drain the render queue. 3. Process ops through the render pipeline. 4. Calculate elapsed time and sleep for the remainder of the 50ms budget. Dirty checking is implemented per window: - Each window maintains a content hash of its current displayed state. - After formatting, the new content hash is compared to the stored hash. - If unchanged, the window is skipped in the Ncurses and Composite stages. - On change, the window is marked dirty, rendered, and its hash updated. ```ruby loop = RenderLoop.new(render_queue, pipeline, fps: 20) loop.start # Runs on dedicated thread loop.stop # Graceful shutdown ``` ## Acceptance Criteria - [ ] `RenderLoop` maintains 20 FPS (50ms per frame) target. - [ ] Frame time compensation: if processing takes 30ms, sleep 20ms. - [ ] If processing exceeds 50ms, the next frame starts immediately (no negative sleep). - [ ] Each window tracks a content hash for dirty checking. - [ ] Unchanged windows are skipped during rendering. - [ ] Changed windows are re-rendered and their hash updated. - [ ] `RenderLoop#start` launches a dedicated render thread. - [ ] `RenderLoop#stop` gracefully shuts down the render thread. - [ ] FPS is configurable (default 20). ## Subtasks ### Code - [ ] Create `RenderLoop` class with `#start` and `#stop` lifecycle methods. - [ ] Implement frame timing with 50ms target and sleep compensation. - [ ] Implement frame overrun handling (skip sleep, proceed immediately). - [ ] Implement per-window content hashing for dirty checking. - [ ] Integrate dirty check into the `DirtyCheckStage` of the pipeline. - [ ] Add configurable FPS parameter (default 20). - [ ] Implement graceful shutdown with thread join. ### Quality - [ ] Docs: Update YARD comments on affected classes and methods. Update relevant Docusaurus documentation pages if applicable. - [ ] Tests (Cucumber): Add `tests/unit/render_fps.feature` covering consistent 20 FPS timing, frame overrun handling, dirty window re-rendering, clean window skipping, configurable FPS, start/stop lifecycle, content hash comparison accuracy. - [ ] Tests (Cucumber Integration): Add integration feature in `tests/integration/` for 20 FPS render loop with dirty checking optimization. - [ ] Tests (Profiling): Run `bundle exec rake unit_profile` and verify no performance regressions. - [ ] Quality: Verify coverage >=97% via `bundle exec rake unit`. If coverage is <97% then review the current unit test coverage report at `build/tests/unit/coverage/` and use it to write new Cucumber based unit tests to improve coverage. Specifically, write Cucumber/Gherkin style unit tests that are descriptively named and specifically improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `bundle exec rake unit` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality: Run `bundle exec rake` (default task: unit tests with coverage) and `bundle exec rake integration`, fix any errors if needed ensuring both pass across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation. - The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done.
freemo added this to the v1.1.0 milestone 2026-03-16 01:59:27 +00:00
freemo self-assigned this 2026-03-16 01:59:27 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Blocks
Reference: aethyr/Aethyr#308
No description provided.