Research Log: Feature Demo Timer Investigation
Research Log: Feature Demo Timer Investigation
Date: March 9, 2026 Duration: ~18 hours continuous session Focus: Investigating why the Feature Demo timer gets stuck at 10 in MAME Issue: kn5000-y7t5 (P1 — Feature Demo SSF visual presentation broken)
Executive Summary
An 18-hour continuous investigation session traced the Feature Demo timer stall to a specific firmware function (SwbtWr_ReinitBothBanks) that runs an inline event dispatch loop, blocking the main loop for many seconds. The root cause was identified but not fully resolved. This report documents the findings, methodology, mistakes, and principles for future investigations.
What Was Being Investigated
The Feature Demo has two independent subsystems:
- Demo Timer System — counts down at DRAM
0x0D2F, cycling through demo songs. At threshold 10, it loads the next song; at 3, it starts playback; at 1, it updates the display. - SSF Visual Presentation — renders FTBMP bitmaps to the LCD via XML-scripted actions.
Both systems were partially broken in MAME. The SSF presentation issue was previously solved (documented on the Feature Demo page). This session focused on the timer system — specifically, why the timer gets stuck at value 10 and never decrements further.
Root Cause Found: SwbtWr_DispatchLoop Blocks the Main Loop
The Discovery
Demo_SelectEntry_PlaySong (file_demo_proc.s:718) is called when the demo timer reaches 10. Its job is to load and start the next demo song. The call chain:
Demo_SelectEntry_PlaySong (0xF86E06)
→ cpdi8 36148, 19 ; check DRAM[0x8D34] == 0x13
→ calr Demo_GetPresetBaseForPartAlt ; read song pointer from table
→ call ToneGen_FileIO_SaveAndSync ; configure song data
→ call SwbtWr_ReinitBothBanks ; ← BLOCKS HERE
→ call Seq_DispatcherEntry ; ← NEVER REACHED
→ stdi8 36686, 6 ; ← NEVER REACHED (0x8F4E stays 0x04)
→ ret
How the Blocking Works
SwbtWr_ReinitBothBanks (style_data_init.s:1405) calls SwbtWr_InitBank1 and SwbtWr_InitBank2. Each of these calls SwbtWr_DispatchLoop_Init, which has a critical fall-through:
SwbtWr_DispatchLoop_Init: ; style_data_init.s, address in dsp_config_sysex.s:735
stdi16 49275, 0 ; reset buffer index to 0
; *** FALLS THROUGH ***
SwbtWr_DispatchLoop: ; dsp_config_sysex.s:738
ldda32 xiy, 49289 ; load buffer base pointer
addda16 xiy, 49275 ; add current index
cp (xiy), 0xFF ; check for terminator
jr z, SwbtWr_DispatchLoop_PostCallbacks
; ... process one event, call tone generator callback ...
adddi16 49275, 4 ; advance index by 4 bytes
jrl SwbtWr_DispatchLoop ; tight loop — no yield
This is a tight processing loop that iterates over all buffered MIDI/sequencer events until it hits a 0xFF terminator byte. Each iteration calls a tone generator callback, which takes approximately 5 frames (~80ms) to complete. With the buffer index climbing from 0 to 140+ entries over 130+ frames, the entire loop takes many seconds to complete.
During this time, the main firmware loop is completely blocked. Demo_SelectEntry_TimerTick is never called, so the timer stays frozen at 10.
Evidence
MAME Lua traces confirmed:
| Metric | Value | Implication |
|---|---|---|
DRAM[0x8F4E] |
Stays at 0x04 |
stdi8 36686, 6 at line 741 never executes |
swbt_idx (DRAM 0xC07B) |
Climbs 0→140+ over 130 frames | Loop processing ~1 event per 5 frames |
Timer (0x0D2F) |
Stuck at 10 | Main loop blocked, TimerTick never called |
chain (0xC080) |
Constant during block | No new events being generated |
Follow-Up: Timer Blocking is Temporary (CORRECTION)
Update (March 9, same day): After rebuilding MAME (to eliminate stale debug messages) and running the trace for 120 seconds instead of 60, the blocking was found to be temporary, not permanent.
| Frame | Event |
|---|---|
| 1225 | Timer reaches 10, PlaySong called |
| 1237 | SwbtWr buffer processing begins (buf_idx starts climbing) |
| 1600 | buf_idx reaches 452, then resets (second bank init) |
| 1686–2100 | Second bank processing cycle (buf_idx 0→488) |
| ~2200 | 8F4E changes to 0x06 — PlaySong RETURNED |
| 2600 | Timer resumes counting: 10→9→8→… |
| 3000 | Timer at 4, system functioning normally |
Total blocking time: ~960 frames (~16 seconds). The dispatch loop processes 452+ events across multiple bank reinitializations. Each callback takes ~35ms, which is consistent with the volume of work.
Previous diagnosis was wrong because the trace only ran for 1500 frames (25 seconds total, only 5 seconds after LEFT 2). The 16-second blocking period hadn’t completed when the trace ended.
Open question: Does the ~16 second blocking match real hardware behavior? On real hardware with waveform ROMs present, the tone generator callbacks may complete faster (or slower), affecting the blocking duration.
What Was NOT Resolved
1. Whether 16-Second Blocking Matches Real Hardware
The ~16 second SwbtWr processing delay may or may not be correct. On real hardware:
- Tone generator writes go to actual DSP hardware (fast)
- In MAME, they go to HLE (potentially different timing)
- The real question is whether users would notice a 16-second pause when entering the Feature Demo
2. SSF Visual Presentation Event Routing
While the timer blocking was traced, the SSF event routing remains separately broken:
- Event
0x1C00038requiresUIState_KeyScan_Dispatchto be called with the correct UI state - LEFT 2 button generates param upper16 =
0xAA0A, which doesn’t match any of the 5 registered filters (0x1000–0x1400) demo_stateat0x0251D8stays0x0000throughout
3. Demo Song Pointer Table
The table at 0x9C4000 in Table Data ROM holds 19 valid entries (indices 0–18). Entry 18 = 0x008E0000. Entry 19 = NULL (end-of-list). The demo cycles through these using index stored at DRAM 0x28A4 (10404). The index was confirmed set to 18 (stda8 10404, a at drawbar_panel_ui.s:15232).
Methodology
Tools Used
- MAME Lua autoboot scripts — 5 custom scripts created:
trace_demo_events.lua— Button sequence + DRAM state monitoringread_demo_table.lua— Pointer table dump + state monitoringtrace_playsong.lua— Timer history + playback state trackingtrace_playsong_detailed.lua— PC + SwbtWr index monitoringtrace_swbt_buffer.lua— Buffer content dump
- ROM disassembly analysis — Cross-referencing assembly source files:
file_demo_proc.s— Demo timer and song loadingdsp_config_sysex.s— SwbtWr dispatch loopstyle_data_init.s— Bank initializationtonegen_voice_ctrl.s— UI state dispatchui_widget_defs.s— Event dispatch routingcpanel_constants.s— Button mappingcpanel_routines.s— Control panel ISR
- Button-to-event tracing — Full path from physical button press through ISR → event queue → dispatch → handler
Approach: Progressive Narrowing
The investigation followed a progressive narrowing pattern:
Timer stuck at 10
→ Which function is called at timer=10?
→ Demo_SelectEntry_PlaySong
→ Which call in PlaySong blocks?
→ SwbtWr_ReinitBothBanks
→ How does ReinitBothBanks block?
→ InitBank → DispatchLoop_Init falls through to DispatchLoop
→ Why is DispatchLoop slow?
→ Each callback takes ~5 frames (UNRESOLVED)
Reflection: Advances and Mistakes
Advances
- Identified the exact blocking function —
SwbtWr_DispatchLooprunning inline via fall-through fromSwbtWr_DispatchLoop_Init, called fromSwbtWr_ReinitBothBanks - Mapped the full call chain from timer tick → PlaySong → ReinitBothBanks → DispatchLoop
- Confirmed with Lua traces that
DRAM[0x8F4E]stays at0x04(PlaySong never returns) - Documented the SwbtWr buffer protocol — 4-byte events,
0xFFterminator, base at0xC089, index at0xC07B - Traced the button-to-event path — CPanel ISR → event queue at
0x200AD→ dispatch → handler
Mistakes
-
Spent too long on ROM byte reading — Tried to manually interleave even/odd Table Data ROM files to read the pointer table. Got wrong byte ordering. Should have immediately used MAME Lua to read through the memory map.
-
Didn’t rebuild MAME early enough — The C07D debug messages that obscured trace output came from a stale binary. Should have verified the MAME binary was current before starting intensive tracing.
-
Explored tangential paths — Spent significant time tracing the SSF event routing (which was already documented as working via a different path) instead of focusing exclusively on the timer blocking.
-
Insufficient scope control — The session drifted between investigating the timer system and re-investigating the SSF event routing, diluting focus on either.
-
Too many Lua scripts — Created 5 separate trace scripts with overlapping functionality. A single well-structured script with configurable sections would have been more efficient.
-
Delayed the “measure, don’t guess” principle — Spent time reading assembly and hypothesizing before writing traces. The Lua traces provided definitive answers much faster than code reading.
-
No intermediate commits — Findings accumulated for hours without being committed to documentation or the issue tracker, risking loss of context.
Principles for Future Investigation Sessions
Based on this session’s experience, the following principles should guide future deep-dive investigations:
1. Measure First, Read Second
Write a Lua trace script within the first 30 minutes. Let the emulator tell you what’s happening before spending hours reading assembly. Assembly reading should be guided by trace data, not the other way around.
2. One Question Per Script
Each Lua trace script should answer exactly one question. Don’t combine buffer dumps, state monitoring, and event tracing in a single script. Focused scripts produce focused answers.
3. Rebuild Before Tracing
Always verify the MAME binary is current before starting any trace session. Run git status in the MAME directory and rebuild if there are uncommitted changes or if the binary is older than the source.
4. Commit Findings Incrementally
After each significant finding, commit it to the documentation and issue tracker immediately. Don’t accumulate hours of undocumented discoveries. A finding that isn’t committed doesn’t exist.
5. Time-Box Tangents
If an investigation path hasn’t yielded results in 2 hours, write down what was tried and switch to a different approach. Don’t follow the same lead for 8+ hours.
6. Separate Issues Stay Separate
The timer blocking and the SSF event routing are different bugs. Investigating both in the same session is fine, but switching between them without closing one first leads to confusion and duplicated work.
7. Use the Right Tool for the Job
- MAME Lua for runtime state observation
llvm-mc --disassemblefor decoding bytes- ROM disassembly source for understanding control flow
llvm-objdumpfor labeled ELF disassembly- Don’t try to manually decode ROM bytes when tools exist
8. Document the Dead Ends
Failed approaches are valuable — they narrow the search space for the next session. Write them down with the reason they didn’t work.
9. Check Assumptions Early
The assumption that LEFT 2 sends event 0x1C00038 via UIState_KeyScan_Dispatch was wrong — it goes through a different path. Testing assumptions with traces before building on them saves hours.
10. Keep a Running Log
Maintain a timestamped log of what was tried, what was found, and what questions remain. This prevents re-exploring paths that were already dead-ended and makes handoff (to another session or another agent) much smoother.
Next Steps
-
Verify SwbtWr buffer terminator — Dump the buffer at
0xBD3Cto check for proper0xFFtermination. If missing, this explains the long blocking. -
Profile tone generator callbacks — Instrument the callback function to measure per-call timing. The ~5-frame-per-event rate may indicate an emulation issue.
-
Rebuild MAME — Clean rebuild to eliminate stale debug messages and ensure the binary matches current source.
-
Test with shorter buffer — If the buffer has hundreds of events, the blocking is by-design behavior (processing a large backlog). The real fix may need to happen in how the buffer is populated.
-
Investigate SSF event routing independently — The
0xAA0Aparameter not matching any filter is a separate issue from the timer blocking.
Key Source Files
| File | Key Functions | Relevance |
|---|---|---|
file_demo_proc.s |
Demo_SelectEntry_TimerTick, Demo_SelectEntry_PlaySong, Demo_SelectEntry_ProcessSongList |
Timer state machine and song loading |
dsp_config_sysex.s |
SwbtWr_DispatchLoop, SwbtWr_QueueMainEvent |
The blocking dispatch loop |
style_data_init.s |
SwbtWr_ReinitBothBanks, main loop Phase 4 |
Initialization that triggers the block |
tonegen_voice_ctrl.s |
UIState_KeyScan_Dispatch |
Button event routing |
ui_widget_defs.s |
EventDispatch_Direct (0xFA9945) |
Event filter matching |
cpanel_constants.s |
Button mapping tables | Physical button → event param |
cpanel_routines.s |
CPanel_RX_ButtonPacket |
Control panel ISR |
seq_task_sched.s |
Seq_DispatcherEntry, Seq_DispatcherTick |
Sequencer task scheduling |
Key DRAM Addresses
| Address | Name | Purpose |
|---|---|---|
0x0D2F |
Demo timer | Countdown value (set to 15, triggers at 10/3/1) |
0x8F4E |
Playback state | Set to 6 by PlaySong (stays at 4 = blocked) |
0x8D34 |
UI state | Must be 0x13 for PlaySong to execute |
0x8D38 |
UI sub-state | 0xE4 = Feature Presentation mode |
0xC07B |
SwbtWr buffer index | Current read position in dispatch buffer |
0xC089 |
SwbtWr buffer base | Pointer to event buffer |
0xC080 |
Chain byte | Handler chain selector |
0xC07D |
Param byte | Handler parameter |
0x0251D8 |
Demo state | SSF presentation state (stays 0x0000) |
0x28A4 |
Demo entry index | Current song index (set to 18) |
0x28B4 |
Guard variable | Song list pointer |
This is an automated research log from a continuous investigation session. Findings are preliminary and some conclusions may be revised as investigation continues.
Last updated: March 9, 2026