Implement draw cache for optimized rendering in TIC-80 - #2975
Conversation
This (at first blush) sounds overengineered... IF we're already comparing ALL the VRAM, VBANK1, sprites, fonts, etc... shouldn't we have reached certainty about whether the screen could be different or not? Why do we also need to walk a history of draw commands? Sounds like a lot of extra complexity. It's also possible the draw calls could have changed while the display didn't change at all.. Yes, edge cases for sure... but... Curious what this part of the system is trying to account for? |
If you profile the application, in the current system, the software renderer Moving the software renderer to GPU is infeasible, due to retro games peek and poke all the time. I actually tried to port the entire software renderer to Metal but due to every time it peek/poke I have to copy the buffer back from GPU to CPU, the metal based renderer runs even slower than software one, for almost all games I tested. So draw cache aim to solve this bottleneck in the software renderer.
the major customer that will enjoy this optimization is all built in editors (code, music, sprite, etc). you will see CPU utilization drop to around 1/25. There are also games don't need 60fps rendering. for instance, my favorite game Cauliflower Power. When you are tackling the puzzle game and the character is standing still, you will see it only redraws 1 frame per second. The various slow games such as puzzle games, large portions of cartridges in Tech Tools Music sections from the official site, will all benefit from this tremendously. |
Why does it do this if no VRAM or registers have changed? Could we just fix the software rendering to be a bit smarter in terms of changes from frame to frame? If there were truly no changes to display then I'd think the software renderer could essentially be a NOOP, no? |
because VRAM is the final state of the software rendering. In retro console, user function ( Once at the stage of comparing VRAMS, the software rendering is already finished --- You cannot rewind the clock to skip that step when you find this frame's rendering is not needed.
The draw cache is fixing exactly this! When nothing needs to be redrawn, all drawing instructions becomes noops. |
|
I recorded a few performance graph for reviewers to understand the benefit of draw cache for quite games and code editing in this comment: #2973 (comment) please take a look I want to post one additional performance graph here. It's recorded when playing stele, a action game where each single frame is different.
so the user program calls On the graph you can find the conclusion is: For dynamic screens, the system has non-measurable overhead. For close to static screens, the system saves 10-25X amount of CPU cycles. |

TIC-80 Draw Cache System
Rationale
In TIC-80, the main loop or editor tick function (
tic()) is called 60 times per second, and by default, every single frame is fully redrawn from scratch, even when the screen contents are completely static (for example, when simply reading code, hovering a stationary mouse, or resting on a menu).Furthermore, many games do not change their state or visual representation on every frame, and some games may only need to update and redraw their screen every 5, 10, or more frames.
Redrawing static frames or slow-running games continuously results in unnecessary and wasteful CPU consumption. The Draw Cache is an optimization layer designed to solve this by bypassing expensive, pixel-by-pixel rendering operations (like drawing text characters or rendering tilemaps) when the visual state of the application has not changed from the previous frame.
How It Works
1. Interception (Hooking)
All standard drawing APIs called by the TIC-80 studio/editors (e.g.
tic_api_cls,tic_api_print,tic_api_rect, etc.) are intercepted in studio.h and routed through caching wrappers defined in draw_cache.c:2. State Validation (Start of Frame)
At the beginning of each frame (
tic_core_draw_cache_start), the cache checks whether the environment is unchanged compared to the previous frame. It compares:If any of these inputs have changed, the draw cache is immediately invalidated for this frame, forcing a full redraw.
3. Record and Match (
handle_draw_call)During the update loop, as drawing commands are called, each command parameters are serialized into a
DrawCallstruct and passed tohandle_draw_call:curr_calls_buf).prev_calls_buf).handle_draw_callreturnsfalse(skip execution). The drawing function is bypassed entirely.handle_draw_callreturnstrue(execute drawing). All subsequent drawing commands for the rest of the frame will be executed.4. Finalization (End of Frame)
At the end of the frame (
tic_core_draw_cache_end):curr_calls_buf.curr_calls_bufare swapped to becomeprev_calls_buffor the next frame.Performance Benefits
draw.c) is the main bottleneck and compare to that the cache operation is very cheap.