You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Clean up the main loop by reordering phases (input before update), separating concerns in _tick() and _update_frame(), and fixing the one-frame input lag in GLFW mode. No new class hierarchies — just reorganization of existing code.
Why not a GameLoop class with fixed timestep and Platform ABC?
The original proposal introduced a GameLoop class with an accumulator-based fixed-timestep pattern, and a Platform ABC with GLFWPlatform / JupyterPlatform subclasses. Both are over-engineered for the current state:
Fixed timestep solves a physics problem rtxpy doesn't have. The accumulator pattern (while accumulator >= fixed_dt) ensures deterministic physics simulation regardless of frame rate. rtxpy doesn't simulate physics — camera movement is delta-time scaled, which is correct for a visualization tool. The existing dt * speed approach gives smooth movement at any frame rate. Fixed timestep would only matter if we added rigid body dynamics (Phase 3), and it can be added then.
Two backends don't justify an ABC. There are exactly two platforms (GLFW and Jupyter) with very different models (callback-based vs. queue-based, OpenGL texture upload vs. JPEG-over-widget). An ABC adds a layer of indirection that makes both harder to understand, and there's no third platform on the horizon. If one appears, the abstraction can be extracted then.
Variable render rate already works. The current loop renders only when _render_needed is true and _frame_dirty gates texture upload. This is already the "render only when dirty" pattern the proposal describes — it just needs clearer phase separation, not a new class.
What IS needed: the loop has real bugs and organizational problems that are worth fixing without new abstractions.
Bug: one-frame input lag.glfw.poll_events() runs AFTER _tick() and rendering. This means key presses and mouse events from the current frame aren't processed until the next frame's _tick(). Moving poll_events() to the top of the loop fixes this.
_tick() method (engine.py:3939-3966+)
Mixes three concerns:
Input processing: delta-time computation, held-key → movement accumulation (lines 3944-3955)
No fixed timestep accumulator. Delta-time scaling works correctly for camera movement. Add fixed timestep only when/if physics simulation is added (Phase 3).
No Platform ABC. GLFW and Jupyter loops stay as separate code paths. They share _tick() and _update_frame() but their loop structures are different enough that an ABC would be forced.
No GameLoop class. The loop is 15 lines of code in a try/finally block. Wrapping it in a class adds indirection without value.
No delta-time refactor. The current dt_scale = dt / 0.05 reference-rate pattern is fine — it gives consistent movement speed. Changing to raw dt * speed_in_units_per_second would require retuning all speed defaults for no user-visible benefit.
Implementation Notes
The glfw.poll_events() reorder is a one-line move with immediate benefit — can land independently
Parent: #57 — Phase 1 Architecture
Summary
Clean up the main loop by reordering phases (input before update), separating concerns in
_tick()and_update_frame(), and fixing the one-frame input lag in GLFW mode. No new class hierarchies — just reorganization of existing code.Why not a GameLoop class with fixed timestep and Platform ABC?
The original proposal introduced a
GameLoopclass with an accumulator-based fixed-timestep pattern, and aPlatformABC withGLFWPlatform/JupyterPlatformsubclasses. Both are over-engineered for the current state:Fixed timestep solves a physics problem rtxpy doesn't have. The accumulator pattern (
while accumulator >= fixed_dt) ensures deterministic physics simulation regardless of frame rate. rtxpy doesn't simulate physics — camera movement is delta-time scaled, which is correct for a visualization tool. The existingdt * speedapproach gives smooth movement at any frame rate. Fixed timestep would only matter if we added rigid body dynamics (Phase 3), and it can be added then.Two backends don't justify an ABC. There are exactly two platforms (GLFW and Jupyter) with very different models (callback-based vs. queue-based, OpenGL texture upload vs. JPEG-over-widget). An ABC adds a layer of indirection that makes both harder to understand, and there's no third platform on the horizon. If one appears, the abstraction can be extracted then.
Variable render rate already works. The current loop renders only when
_render_neededis true and_frame_dirtygates texture upload. This is already the "render only when dirty" pattern the proposal describes — it just needs clearer phase separation, not a new class.What IS needed: the loop has real bugs and organizational problems that are worth fixing without new abstractions.
Current State
GLFW Desktop Loop (engine.py:6206-6243)
Bug: one-frame input lag.
glfw.poll_events()runs AFTER_tick()and rendering. This means key presses and mouse events from the current frame aren't processed until the next frame's_tick(). Movingpoll_events()to the top of the loop fixes this._tick()method (engine.py:3939-3966+)Mixes three concerns:
_render_neededtriggering_update_frame()method (engine.py:5114-5238)Mixes two concerns:
Jupyter Loop (notebook.py:329-371)
The Jupyter loop already has better phase ordering (input before tick), but mixes rate limiting with presentation.
Proposed Changes
1. Move
glfw.poll_events()before_tick()Single-line fix that eliminates the one-frame input lag:
2. Split
_tick()into clear sectionsNot new methods for the sake of methods — just clear section comments and grouping:
This aligns with the composition approach from #61 —
_tick()delegates to subsystem objects instead of inlining everything.3. Extract
_drain_command_queue()and_present_if_dirty()Pull the inline REPL drain and texture upload into named methods for readability:
4. Split
_update_frame()render vs. present concernsThe async GPU readback naturally separates render from present:
What NOT to change
_tick()and_update_frame()but their loop structures are different enough that an ABC would be forced.try/finallyblock. Wrapping it in a class adds indirection without value.dt_scale = dt / 0.05reference-rate pattern is fine — it gives consistent movement speed. Changing to rawdt * speed_in_units_per_secondwould require retuning all speed defaults for no user-visible benefit.Implementation Notes
glfw.poll_events()reorder is a one-line move with immediate benefit — can land independently_tick()reorganization depends on Phase 1: Decompose InteractiveViewer via Composition #61 (composition) for the subsystem delegation calls_drain_command_queue()extraction is a pure refactor — no behavior change_present_if_dirty()extraction is a pure refactor — no behavior change_update_frame()split is the most involved change but follows the existing async readback boundary_tick()cleanup but its loop structure stays separateAcceptance Criteria
glfw.poll_events()called before_tick()— no one-frame input lag_tick()has clear input/simulation/render sections (comments or subsystem delegation)_drain_command_queue()_present_if_dirty()_update_frame()separates render (ray trace + post-process) from present (readback + composite)explore()functionality preserved — no user-visible behavior change