Bake Layer Persistence Fix#
Date: October 1, 2025
Issue: The bake command doesn't persist across frames like embedded layers do
File: system/public/aesthetic.computer/lib/kidlisp.mjs
Problem Description#
When using the bake command in KidLisp (e.g., in $fezw), the baked layers don't work the same way as kidlisp embedded pasting. After bake runs, subsequent drawing commands should appear on a transparent layer over the baked content, and that state should persist across frames.
Example Code (from $fezw)#
1 fade:red-green-blue-green-red
2 ink zebra 32, bunch 8 point
3 scroll -1
4 (1s (zoom 0.5))
5 bake
6 ink white
7 line 0 0 w h
Expected Behavior:
- Lines 1-4 create animated content
- Line 5 (
bake) captures this content into a persistent background layer - Lines 6-7 draw on a transparent layer OVER the baked content
- The baked layer persists across frames
- New drawing commands continue to appear on top of the baked layer
Current Behavior:
- The baked layer is cleared every frame
- Drawing after
bakedoesn't appear layered over the baked content - The effect doesn't persist like embedded layers do
Root Cause Analysis#
How bake Currently Works#
-
Bake Function (lines 5440-5499):
- Captures current screen buffer into
this.bakedLayers[] - Uses
this.onceExecuted.add("bake_call")to prevent repeated calls - Optionally clears the screen buffer after baking
- Stores pixel data:
new Uint8ClampedArray(api.screen.pixels)
- Captures current screen buffer into
-
Rendering Baked Layers (lines 8612-8664):
renderBakedLayers()composites baked layers underneath current content- Uses alpha blending: only shows baked pixels where current pixels are transparent
- Called during frame execution (line 2645)
-
THE PROBLEM (lines 1419-1421 in
reset()method):// Reset baked layers state this.bakedLayers = []; this.bakeCallCount = 0;The baked layers are cleared every frame in
reset()!
How embed Layers Work (for comparison)#
-
Embedded Layer Storage:
- Stored in
this.embeddedLayers[]andthis.embeddedLayerCache(lines 737-738) - NOT cleared in
reset()method - they persist across frames - Only cleared when explicitly requested (line 1376 comment)
- Stored in
-
Rendering Embedded Layers (lines 9415+):
renderEmbeddedLayers()re-evaluates and re-renders each frame- Layers persist and are re-rendered with updated frame state
- Complex optimization logic for when to re-evaluate vs just re-paste
-
Lifecycle Management:
- Cleared in
clearEmbeddedLayerCache()method - Explicitly controlled, not automatically reset
- Cleared in
Solution Design#
Primary Fix: Don't Clear Baked Layers in reset()#
The reset() method should NOT clear this.bakedLayers. This array should persist across frames like embedded layers do.
Rationale:
- The
onceExecutedset already prevents re-baking on each frame - The baking operation is meant to be a one-time capture that persists
- Similar to how embedded layers work - they persist until explicitly cleared
- The rendering pipeline already handles compositing baked layers correctly
Changes Required#
1. Remove Baked Layer Clearing from reset() (lines 1419-1421)#
// BEFORE:
// Reset baked layers state
this.bakedLayers = [];
this.bakeCallCount = 0;
// AFTER:
// Don't reset baked layers - they should persist across frames like embedded layers
// Baked layers are only cleared when source code changes or explicitly requested
// this.bakedLayers = []; // REMOVED
// this.bakeCallCount = 0; // REMOVED (or keep for tracking but don't clear layers)
2. Clear Baked Layers When Source Changes#
Baked layers should be cleared when:
- Source code changes (when
clearOnceExecutedis true inreset()) - Module changes (similar to embedded layers)
- Explicitly requested via a clear/reset command
Add to reset() method:
// Clear onceExecuted only when explicitly requested (when source changes)
if (clearOnceExecuted) {
this.onceExecuted.clear();
// Also clear baked layers when source changes
this.bakedLayers = [];
this.bakeCallCount = 0;
}
3. Optional: Add Method to Explicitly Clear Baked Layers#
Add a convenience method for clearing baked layers:
clearBakedLayers() {
this.bakedLayers = [];
this.bakeCallCount = 0;
this.onceExecuted.delete("bake_call");
console.log("🍞 Cleared all baked layers");
}
This could be called from module() or other lifecycle methods as needed.
Implementation Steps#
-
Modify
reset()method (around line 1419):- Remove unconditional clearing of
this.bakedLayers - Move clearing into the
if (clearOnceExecuted)block
- Remove unconditional clearing of
-
Test the fix:
- Run the example code from
$fezw - Verify baked content persists across frames
- Verify new drawing appears on top of baked content
- Verify baking only happens once
- Run the example code from
-
Edge case handling:
- Ensure baked layers are cleared when source changes
- Verify screen resize doesn't break baked layer compositing
- Check memory usage with multiple bake calls
Expected Behavior After Fix#
Using the example code:
fade:red-green-blue-green-red
ink zebra 32, bunch 8 point
scroll -1
(1s (zoom 0.5))
bake
ink white
line 0 0 w h
-
First frame execution:
- Lines 1-4 create animated zebra pattern with fade and scroll
- Line 5 captures this into
bakedLayers[0] - Line 5 clears the screen buffer (optional, based on args)
- Lines 6-7 draw white line on fresh transparent buffer
-
Subsequent frames:
onceExecutedprevents re-execution ofbakerenderBakedLayers()composites the baked zebra pattern- Lines 1-4 continue to animate (scroll, fade, zoom)
- Lines 6-7 draw white line on top
- Result: animated background with static white line overlay
Key difference: The baked layer persists and is re-composited each frame, rather than being cleared and lost.
Alternative Approaches Considered#
1. Make bake work like embed with re-evaluation#
- More complex, would require creating a KidLisp instance per baked layer
- Overkill for the use case -
bakeis meant to capture static state - Would change the semantics of
bakesignificantly
2. Add a persist flag to bake#
(bake)- clear layers each frame (current behavior)(bake persist)- keep layers across frames (new behavior)- More flexible but adds API complexity
- Not needed - persistence should be the default behavior
3. Use a separate command like freeze or snapshot#
- Keep
bakeas-is, add new command for persistent layers - Redundant -
bakeshould work correctly rather than adding another command - Increases API surface without clear benefit
References#
- Bake function definition: lines 5438-5499
- Bake rendering: lines 8612-8664 (
renderBakedLayers(),compositeBakedLayer()) - Bake call in frame pipeline: line 2645
- Reset method: lines 1400-1430
- Embedded layers for comparison: lines 737-738, 9415+ (
renderEmbeddedLayers())
Success Criteria#
✅ Baked layers persist across frames
✅ Subsequent drawing commands appear on transparent layer over baked content
✅ Baking only happens once per program execution (via onceExecuted)
✅ Baked layers are cleared when source code changes
✅ No memory leaks or performance regressions
✅ Example code from $fezw works as expected
Implementation Status#
COMPLETED - October 1, 2025
Phase 1: Layer Persistence (Completed)#
-
Modified
reset()method (lines ~1406-1425):- Moved
this.bakedLayers = []andthis.bakeCallCount = 0inside theif (clearOnceExecuted)block - Added comment explaining that baked layers persist across frames like embedded layers
- Baked layers now only clear when source code changes
- Moved
-
Added
clearBakedLayers()method (after line 8968):clearBakedLayers() { this.bakedLayers = []; this.bakeCallCount = 0; this.onceExecuted.delete("bake_call"); console.log("🍞 Cleared all baked layers"); } -
Called
clearBakedLayers()in constructor (line ~772):- Added call after
clearEmbeddedLayerCache()during initialization
- Added call after
-
Called
clearBakedLayers()inmodule()method (line ~2382):- Added call after
clearEmbeddedLayerCache()when loading new modules - Ensures fresh state when entering/re-entering a piece
- Added call after
Phase 2: Drawing Suppression (Completed)#
The persistence fix alone wasn't enough - code before bake was still executing and drawing every frame. Added a suppression mechanism to prevent drawing operations before the bake point on subsequent frames:
-
Added flags to constructor (lines ~897-900):
this.hasBakedContent = false; // Track if bake has been called this.suppressDrawingBeforeBake = false; // Suppress drawing before bake point -
Modified
bake()function (lines ~5448-5510):- Sets
this.hasBakedContent = truewhen baking - On subsequent frames, sets
this.suppressDrawingBeforeBake = falsewhen reached - CRITICAL FIX: On subsequent frames, clears the screen buffer (
pixels.fill(0)) before returning - This ensures post-bake drawing starts with a clean transparent buffer
- Without this, post-bake content would composite with previous frame's content
- This creates a "bake point" that divides the code
- Sets
-
Updated
reset()method (lines ~1414-1424):- If
hasBakedContentis true, setssuppressDrawingBeforeBake = trueat frame start - When evaluation reaches
bake, flag is reset tofalse - This suppresses drawing before bake, allows drawing after bake
- If
-
Added suppression checks to drawing AND effect functions:
- Drawing functions:
line,box,circle,wipe,paste,stamp,write,backdrop,flood - Effect functions:
scroll,zoom,suck,blur,contrast - Each function now returns early if
this.suppressDrawingBeforeBakeis true - Critical: Effects like
scrollandzoomtransform the entire screen buffer, so they must also be suppressed to prevent affecting content drawn afterbake
- Drawing functions:
-
Updated
clearBakedLayers()(lines ~8988-8998):- Resets both
hasBakedContentandsuppressDrawingBeforeBakeflags
- Resets both
-
Fixed alpha compositing for baked layers (lines ~8674-8720):
- Changed from simple transparent pixel replacement to proper alpha blending
- Uses formula:
result = current * alpha_current + baked * alpha_baked * (1 - alpha_current) - Ensures baked layer appears as background with current content properly composited on top
How It Works#
First Frame (when bake is called):
- Code before
bakeexecutes normally and draws bakecaptures the screen bufferhasBakedContentis set totrue- Screen is optionally cleared
- Code after
bakeexecutes and draws
Subsequent Frames:
reset()seeshasBakedContent = true, setssuppressDrawingBeforeBake = true- Code before
bakeexecutes but drawing functions return early (suppressed) - When
bakeis reached, setssuppressDrawingBeforeBake = false - Code after
bakeexecutes and draws normally renderBakedLayers()composites the baked layer underneath current content
Result: Content before bake is frozen in the baked layer, content after bake animates on top.
Testing Notes#
Test with the example code from $fezw:
fade:red-green-blue-green-red
ink zebra 32, bunch 8 point
scroll -1
(1s (zoom 0.5))
bake
ink white
line 0 0 w h
Expected behavior:
- Animated zebra pattern with fade and scroll runs once, gets baked (lines 1-4)
- White line draws on top of baked pattern (lines 6-7)
- Baked pattern persists across all frames (static background)
- White line continues to draw on transparent layer on each frame
- Effect is similar to two embedded KidLisp pieces layered on top of each other