Skip to content

Fix CPU spike when mouse is inside window and stationary - #2972

Open
Wang-Yue wants to merge 1 commit into
nesbox:mainfrom
Wang-Yue:pr/fix-cursor-cpu-spike
Open

Fix CPU spike when mouse is inside window and stationary#2972
Wang-Yue wants to merge 1 commit into
nesbox:mainfrom
Wang-Yue:pr/fix-cursor-cpu-spike

Conversation

@Wang-Yue

Copy link
Copy Markdown
Contributor

Troubleshooting:

  • Profiled the TIC-80 main thread under different mouse states and observed that active CPU usage spikes from ~2% (outside window) to ~11% (inside window, stationary).
  • Verified that frame rate remains a steady 38 FPS in both states.
  • Traced the C draw cache validation (draw_cache.c) and discovered that match_a (RAM block A, containing VRAM/tiles/map) was failing on every single frame when the mouse was inside.
  • Instrumented the C block comparison to log the first differing offset. Found that offset 16379 (0x3FFB) was changing on every frame. This offset corresponds to vars.cursor.sprite in VRAM.
  • Traced the lifecycle of vars.cursor:
    1. At the start of every frame, processMouseStates resets it to the default arrow cursor.
    2. During the frame update, the active editor processes mouse hover and sets it back to a custom cursor (like ibeam or hand).
    3. This reset-and-override loop ran on every single frame, modifying RAM and invalidating the draw cache.

Fix:

  • Added a mouse.cursor state tracking structure to the Studio definition in studio.c to decouple cursor state updates from direct VRAM/RAM writes.
  • Modified processMouseStates and setCursor to update the Studio state rather than modifying RAM directly.
  • Implemented cursor state synchronization in renderStudio right before tic_core_tick_end, only writing the cursor sprite to RAM if it has actually changed since the last frame.
  • This prevents VRAM modifications on stationary mouse frames, allowing the draw cache to hit and reducing main thread active tick game CPU usage by over 25x.

Note:

  • The performance measurement above was done with Draw Cache and the new Apple backend. With Draw Cache but other backends, measurements also show significant speed up. The fix will have no effect without the Draw Cache PR.

Troubleshooting:
- Profiled the TIC-80 main thread under different mouse states and observed that active CPU usage spikes from ~2% (outside window) to ~11% (inside window, stationary).
- Verified that frame rate remains a steady 38 FPS in both states.
- Traced the C draw cache validation (draw_cache.c) and discovered that match_a (RAM block A, containing VRAM/tiles/map) was failing on every single frame when the mouse was inside.
- Instrumented the C block comparison to log the first differing offset. Found that offset 16379 (0x3FFB) was changing on every frame. This offset corresponds to vars.cursor.sprite in VRAM.
- Traced the lifecycle of vars.cursor:
  1. At the start of every frame, processMouseStates resets it to the default arrow cursor.
  2. During the frame update, the active editor processes mouse hover and sets it back to a custom cursor (like ibeam or hand).
  3. This reset-and-override loop ran on every single frame, modifying RAM and invalidating the draw cache.

Fix:
- Added a mouse.cursor state tracking structure to the Studio definition in studio.c to decouple cursor state updates from direct VRAM/RAM writes.
- Modified processMouseStates and setCursor to update the Studio state rather than modifying RAM directly.
- Implemented cursor state synchronization in renderStudio right before tic_core_tick_end, only writing the cursor sprite to RAM if it has actually changed since the last frame.
- This prevents VRAM modifications on stationary mouse frames, allowing the draw cache to hit and reducing main thread active tick game CPU usage by over 25x.
@joshgoebel

Copy link
Copy Markdown
Collaborator

This reset-and-override loop ran on every single frame, modifying RAM and invalidating the draw cache.

Please explain how this relates to the draw cache exactly - and what the cost here is with no draw cache. Most games redraw every single frame, so I'm confused why copying a few bytes of memory multiple times would have any real cost at all...

@Wang-Yue

Copy link
Copy Markdown
Contributor Author

Please explain how this relates to the draw cache exactly -

In every frame, draw cache memcpy memory at tic_core_draw_cache_end, and memcmp that with tic_core_draw_cache_start. If memcmp shows they don't match, it will call tic_core_draw_cache_invalidate.

The issue with previous cod is, draw cache will always get invalidated in studio, due to the latter set it to an non-final value during end to start, and reset it to it's correct state during start and end . So if you are in the code editor for instance, it will invalidate the cache every single frame.

and what the cost here is with no draw cache.

I explained in the description: The fix will have no effect without the Draw Cache PR.

Most games redraw every single frame,

games are not affected by this kind of issue, as they strictly redraw during TIC(), which is between start and end, never during end to start, which won't invalidate the cache if they set memory from a value to another and set back. Draw cache still works under this case.

so I'm confused why copying a few bytes of memory multiple times would have any real cost at all...

This has huge cost when using the studio in low power devices such as iPhones or iPads. Say when you carry your mobile device during a travel and want to develop a game on the go, you will have 10-25X difference on battery drain in the code/music/sprite editing mode. Without this patch, editor just runs like a 60fps action game. With this patch applied, it only do redraw then cursor blinks.

@joshgoebel

Copy link
Copy Markdown
Collaborator

The fix will have no effect without the Draw Cache PR.

Yeah, I think that's one of the issues I was having - just trying to understand it from reading the patch in isolation.


Stiill confused if there is a comparison (and from loop to loop the mouse VRAM isn't truly changing then that comparison should match and therefore it shouldn't be redrawn in the first place)?

@Wang-Yue

Copy link
Copy Markdown
Contributor Author

Stiill confused if there is a comparison (and from loop to loop the mouse VRAM isn't truly changing then that comparison should match and therefore it shouldn't be redrawn in the first place)?

previous loop

while(true) {
  other code set mouse to sprite/status A
  tic_core_tick_start()
  call user TIC() function
  set mouse to sprite/status B if we found A is not the right choice
  tic_core_tick_end()  
  composite the image and send to GPU
}

current loop

while(true) {
  record that outer function set mouse to sprite/status A
  tic_core_tick_start()
  call user TIC() function
  read the record, ah it's A, if A is the right choice, we set to A, otherwise, set mouse to sprite/status B
  tic_core_tick_end()  
  composite the image and send to GPU
}

the difference between the two is, we now have all status change happening within tic_core_tick_start/end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants