Monorepo for Aesthetic.Computer aesthetic.computer

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 bake doesn't appear layered over the baked content
  • The effect doesn't persist like embedded layers do

Root Cause Analysis#

How bake Currently Works#

  1. 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)
  2. 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)
  3. 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)#

  1. Embedded Layer Storage:

    • Stored in this.embeddedLayers[] and this.embeddedLayerCache (lines 737-738)
    • NOT cleared in reset() method - they persist across frames
    • Only cleared when explicitly requested (line 1376 comment)
  2. 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
  3. Lifecycle Management:

    • Cleared in clearEmbeddedLayerCache() method
    • Explicitly controlled, not automatically reset

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 onceExecuted set 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 clearOnceExecuted is true in reset())
  • 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#

  1. Modify reset() method (around line 1419):

    • Remove unconditional clearing of this.bakedLayers
    • Move clearing into the if (clearOnceExecuted) block
  2. 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
  3. 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
  1. 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
  2. Subsequent frames:

    • onceExecuted prevents re-execution of bake
    • renderBakedLayers() 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 - bake is meant to capture static state
  • Would change the semantics of bake significantly

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 bake as-is, add new command for persistent layers
  • Redundant - bake should 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)#

  1. Modified reset() method (lines ~1406-1425):

    • Moved this.bakedLayers = [] and this.bakeCallCount = 0 inside the if (clearOnceExecuted) block
    • Added comment explaining that baked layers persist across frames like embedded layers
    • Baked layers now only clear when source code changes
  2. Added clearBakedLayers() method (after line 8968):

    clearBakedLayers() {
      this.bakedLayers = [];
      this.bakeCallCount = 0;
      this.onceExecuted.delete("bake_call");
      console.log("🍞 Cleared all baked layers");
    }
    
  3. Called clearBakedLayers() in constructor (line ~772):

    • Added call after clearEmbeddedLayerCache() during initialization
  4. Called clearBakedLayers() in module() method (line ~2382):

    • Added call after clearEmbeddedLayerCache() when loading new modules
    • Ensures fresh state when entering/re-entering a piece

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:

  1. Added flags to constructor (lines ~897-900):

    this.hasBakedContent = false; // Track if bake has been called
    this.suppressDrawingBeforeBake = false; // Suppress drawing before bake point
    
  2. Modified bake() function (lines ~5448-5510):

    • Sets this.hasBakedContent = true when baking
    • On subsequent frames, sets this.suppressDrawingBeforeBake = false when 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
  3. Updated reset() method (lines ~1414-1424):

    • If hasBakedContent is true, sets suppressDrawingBeforeBake = true at frame start
    • When evaluation reaches bake, flag is reset to false
    • This suppresses drawing before bake, allows drawing after bake
  4. 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.suppressDrawingBeforeBake is true
    • Critical: Effects like scroll and zoom transform the entire screen buffer, so they must also be suppressed to prevent affecting content drawn after bake
  5. Updated clearBakedLayers() (lines ~8988-8998):

    • Resets both hasBakedContent and suppressDrawingBeforeBake flags
  6. 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):

  1. Code before bake executes normally and draws
  2. bake captures the screen buffer
  3. hasBakedContent is set to true
  4. Screen is optionally cleared
  5. Code after bake executes and draws

Subsequent Frames:

  1. reset() sees hasBakedContent = true, sets suppressDrawingBeforeBake = true
  2. Code before bake executes but drawing functions return early (suppressed)
  3. When bake is reached, sets suppressDrawingBeforeBake = false
  4. Code after bake executes and draws normally
  5. 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