Skip to content

Fix #167 (68060 half): decline cleanly when the 68060 has no FPU - #169

Merged
sidick merged 1 commit into
mainfrom
fix-167-68060-no-fpu
Oct 19, 2026
Merged

sidick merged 1 commit into
mainfrom
fix-167-68060-no-fpu

Conversation

@sidick

@sidick sidick commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Scope

This closes the second half of #167 -- the --cpu 68060 --no-fpu crash. The first half (--cpu 68040 --no-fpu) was already fixed and merged separately. With this PR, issue #167 as filed is fully addressed.

The bug

--cpu 68060 --no-fpu died partway through a run with:

continuation stub trapped at 0x000000c4 with no pending continuation (guest jumped to the trampoline stub directly, or ContinuationStack bookkeeping is out of sync)

Empirical root cause (verified, not just hand-derived)

I built a repro test driving the real Runtime/dispatch path (not the bare M68kCpu unit-test harness, which never installs the trap table and can't reproduce this): a CpuType::M68060, fpu_present = false runtime whose only instruction is a real F-line FPU opcode (0xF200, 0x0000).

Before the fix, this produced exactly:

Err(Dispatch(EmptyContinuationStack { pc: 196 }))   // 196 = 0xC4 = CONTINUATION_STUB_ADDR

This confirms the hand-derived mechanism exactly: for 68020/68030/68040, an FPU-absence trap surfaces through the m68k crate's host-interceptable StepResult::FlineTrap/CycleBatchExit::FlineTrap escape hatch, which Cpu::take_hardware_exception (crates/volamos-core/src/backend.rs) catches and declines cleanly if the guest never installed a handler. The 68060's on-die-FPU-disabled exception doesn't go through that hatch at all: the m68k crate's own decode.rs/fpu/operations.rs call cpu.take_fp_disabled_060(bus) directly, bypassing take_hardware_exception entirely, and jump straight through vector 11's slot (guest address 0x2C). That slot is deliberately left genuinely zero (so a guest installing its own real FPU-probe handler there gets real semantics) -- so with no handler installed (the ordinary case for a -m68020-60 -m68881 build that never probes), PC is set to 0 and falls through the deliberately-zeroed exception-vector-table span until it happens to reach CONTINUATION_STUB_ADDR (0xC4) and executes it directly.

The fix

Pre-seed vector 11's slot with the address of a new, dedicated stub (FPU_DISABLED_060_DECLINE_STUB_ADDR, holding a single ILLEGAL opcode) whenever the resolved CPU is CpuType::M68060 with fpu_present == false. This surfaces through the existing StepResult::IllegalInstruction/CycleBatchExit::IllegalInstruction path with zero new plumbing -- turning the crash into a clean, attributable unexpected CPU stop ... Illegal report.

If the guest later installs its own real handler at 0x2C (the FPU-probe idiom), that ordinary guest write simply overwrites the pre-seed like any other guest memory write -- no special-casing needed, verified with a dedicated test.

Where the seeding logic lives, and why

New shared helper: install_fpu_disabled_060_decline_stub(&mut Runtime<C>, cpu_type, fpu_present) in crates/volamos-core/src/dispatch.rs. It has to run after Runtime::new, not before: Runtime::new's own "unknown call" sentinel prefill sweep covers this address range and would clobber an earlier write (confirmed by first writing it before construction and watching the test fail with the sentinel opcode instead).

Called from crates/volamos/src/main.rs at every real Runtime construction site:

  • Both of run()'s loading strategies, via build_runtime_with_vfs (one call site covers both).
  • The nested System()/Execute() path. I extracted the construction-only half of run_nested_program into a new build_nested_runtime returning Option<Runtime<M68kCpu>>, specifically so this could be tested directly -- run_nested_program's own i32 return collapses every construction failure and every runtime error to -1, which made this fix unobservable at that boundary.

This matches attn_flags_for's own established precedent (see its doc comment) for why the CLI, not Runtime/StartConfig, computes CPU-model-dependent guest state: Runtime/StartConfig don't otherwise know or care which concrete CpuType/FPU configuration is in use, and this keeps it that way.

Address bookkeeping

TRAP_TABLE_SIZE grows by 4 bytes (0x2C00 -> 0x2C04): one ILLEGAL opcode word plus one padding word, keeping TRAP_TABLE_END 4-byte aligned (an invariant Runtime::new's heap-start rounding relies on -- growing by just 2 bytes broke heap_starts_at_load_end, caught it before pushing). FPU_DISABLED_060_DECLINE_STUB_ADDR = TRAP_TABLE_END - 2, right after EXIT_STUB_ADDR's own 4-byte slot.

I did not add this to EXEC_LIBRARY_BASE's "Reserved-region memory map" ASCII diagram -- that diagram only ever documented the original 0x1200-end region; every later TRAP_TABLE_SIZE growth (the math libraries, timer.device, locale.library, intuition.library, bsdsocket.library, graphics.library, the SetFunction stub pool) got its own doc comment instead, following the same convention this PR follows for the new constant.

Tests (volamos-core: 1109 -> 1112; volamos: 159 -> 160)

  1. dispatch::tests::m68060_no_fpu_fpu_instruction_repro_167 -- the empirical repro above, now asserting the fixed clean-trap behavior. Doc comment records the exact pre-fix failure for posterity.
  2. dispatch::tests::install_fpu_disabled_060_decline_stub_is_a_no_op_for_every_other_combination -- M68020/M68030/M68040 with fpu=false and M68060 with fpu=true all leave vector 11 genuinely zero.
  3. dispatch::tests::guest_installed_fpu_handler_runs_instead_of_the_decline_stub -- a guest-installed real handler at 0x2C, written after the pre-seed, runs instead of the stub (both by trap pc and by an observable D0 side effect).
  4. tests::nested_runtime_68060_no_fpu_gets_a_clean_trap_not_the_continuation_stub (in main.rs, via the new build_nested_runtime) -- the nested System()/Execute() construction path gets the same pre-seed, exercised with a minimal hand-built hunk executable.

All green:

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace

Explicitly out of scope

This is a volamos-side mitigation that turns a confusing crash into a clean, attributable trap -- it does not fix the underlying m68k crate design gap (68060 FPU-disabled exception delivery bypassing the host's interception hook entirely, unlike every other modeled FPU-absence path). Worth reporting upstream against m68k-rs separately, same as the already-merged 68040 half of this issue noted the 68060 crash as a separate, deeper issue.

Not merging -- leaving that for review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Godbk9UDW5obqYsdkXkyRR

…an FPU

--cpu 68060 --no-fpu crashed partway through a run with "continuation
stub trapped ... with no pending continuation". Root cause, confirmed
with a repro test against the real Runtime/dispatch path (not just the
bare M68kCpu unit-test harness): the m68k crate's internal 68060
FPU-disabled exception delivery (take_fp_disabled_060, called directly
from decode.rs/fpu/operations.rs) bypasses Cpu::take_hardware_exception
entirely, unlike every other modeled FPU-absence path (020/030/040,
which surface through the host-interceptable FlineTrap escape hatch).
It jumps straight through vector 11's slot (guest address 0x2C), which
this runtime deliberately leaves genuinely zero so a guest that installs
its own real FPU-probe handler there gets real semantics. With no
handler installed (the ordinary case for a -m68020-60 -m68881 build that
never probes), PC is set to 0 and falls through the deliberately-zeroed
exception-vector-table span until it happens to reach
CONTINUATION_STUB_ADDR (0xC4) and executes it directly -- exactly what
the confusing error reports.

Fix: pre-seed vector 11's slot with the address of a new, dedicated
ILLEGAL-opcode stub (FPU_DISABLED_060_DECLINE_STUB_ADDR) whenever the
resolved CPU is M68060 with fpu_present == false. This surfaces through
the existing IllegalInstruction trap path with no new plumbing, turning
the crash into a clean, attributable "unexpected CPU stop ... Illegal"
report. If the guest later installs its own real handler at 0x2C (the
FPU-probe idiom), that ordinary write simply overwrites the pre-seed --
no special-casing needed.

The seeding has to run *after* Runtime::new (its own sentinel prefill
sweep covers this address and would clobber an earlier write), via a new
install_fpu_disabled_060_decline_stub(&mut Runtime, cpu_type, fpu)
shared helper called from every real Runtime construction site in
main.rs: both of run()'s loading strategies (through
build_runtime_with_vfs) and the nested System()/Execute() path (through
a newly-extracted build_nested_runtime, factored out of
run_nested_program so its construction can be tested directly -- the
old i32-collapsing return made this fix unobservable at that boundary).
This mirrors attn_flags_for's own precedent for why the CLI -- not
Runtime/StartConfig -- computes CPU-model-dependent guest state.

TRAP_TABLE_SIZE grows by 4 bytes (0x2C00 -> 0x2C04) to fit the new stub
while keeping TRAP_TABLE_END 4-byte aligned.

Tests added (volamos-core: 1109 -> 1112; volamos: 159 -> 160):
- empirical repro against the real Runtime/dispatch path, asserting the
  fixed clean-trap behavior (first run, pre-fix, confirmed the
  hand-derived EmptyContinuationStack{pc:0xC4} mechanism exactly)
- every other cpu_type/fpu combination leaves vector 11 genuinely zero
- a guest-installed real handler at 0x2C runs instead of the stub
- the nested System()/Execute() construction path gets the same pre-seed

Out of scope: the underlying m68k crate design gap (68060 FPU-disabled
exception delivery bypassing the host interception hook) is not fixed
here -- a volamos-side mitigation only, same as the already-merged
68040 half of this issue. Worth reporting upstream separately.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Godbk9UDW5obqYsdkXkyRR
@sidick
sidick merged commit a4f6989 into main Oct 19, 2026
9 checks passed
@sidick
sidick deleted the fix-167-68060-no-fpu branch October 19, 2026 15:34
sidick added a commit that referenced this pull request Oct 19, 2026
PR #169 fixed this but didn't touch userdocs; both the Changelog and
CLI-Reference still described it as a separate, still-open crash.


Claude-Session: https://claude.ai/code/session_01Godbk9UDW5obqYsdkXkyRR

Co-authored-by: Claude Sonnet 5 <[email protected]>
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.

1 participant