From 85890d612458d3ca0e8f6fe3a6ff443aaa81ec00 Mon Sep 17 00:00:00 2001 From: Takumi Sueda Date: Mon, 7 Sep 2026 17:32:38 +0900 Subject: [PATCH 1/2] Simulate ARM9 bus starvation during ARM7 BIOS bulk transfers With ARM7 main RAM priority (EXMEMCNT bit 15), a BIOS CpuSet/CpuFastSet from the ARM7 into main RAM keeps the bus busy for the whole transfer and the ARM9 is starved. melonDS has no bus arbitration, so the ARM9 kept running at full speed. The TWL SDK relies on the starvation: its ARM7 startup zero-fills 0x02000000-0x022A0000 in 192KB chunks (one per VBlank) after the ARM9 title has started. Without it the ARM9 builds its heap inside the region being cleared and loses heap metadata. DSi PictoChat then fails WM_Init with a NULL buffer and shuts down with "Communication error". UpdateBusStarve() stops the ARM9 (CPUStop_ARM9BusStarve) when the ARM7 starts such a transfer of at least 4KB, and DecreaseARM7BytesToWrite() releases it once the transfer's bytes have been written. Interpreter mode only, at present. --- src/ARM.cpp | 5 +++++ src/ARMInterpreter.cpp | 7 +++++++ src/NDS.cpp | 47 ++++++++++++++++++++++++++++++++++++++++++ src/NDS.h | 8 +++++++ 4 files changed, 67 insertions(+) diff --git a/src/ARM.cpp b/src/ARM.cpp index 3937a994fc..8b4a96cadc 100644 --- a/src/ARM.cpp +++ b/src/ARM.cpp @@ -1154,6 +1154,8 @@ void ARMv4::DataRead32S(u32 addr, u32* val) void ARMv4::DataWrite8(u32 addr, u8 val) { + // decrease ARM7 write count if the write targets the main RAM, which is starving bus + if (NDS.ARM7BytesToWrite && (addr >> 24) == 0x02) NDS.DecreaseARM7BytesToWrite(1); BusWrite8(addr, val); DataRegion = addr; DataCycles = NDS.ARM7MemTimings[addr >> 15][0]; @@ -1161,6 +1163,7 @@ void ARMv4::DataWrite8(u32 addr, u8 val) void ARMv4::DataWrite16(u32 addr, u16 val) { + if (NDS.ARM7BytesToWrite && (addr >> 24) == 0x02) NDS.DecreaseARM7BytesToWrite(2); addr &= ~1; BusWrite16(addr, val); @@ -1170,6 +1173,7 @@ void ARMv4::DataWrite16(u32 addr, u16 val) void ARMv4::DataWrite32(u32 addr, u32 val) { + if (NDS.ARM7BytesToWrite && (addr >> 24) == 0x02) NDS.DecreaseARM7BytesToWrite(4); addr &= ~3; BusWrite32(addr, val); @@ -1179,6 +1183,7 @@ void ARMv4::DataWrite32(u32 addr, u32 val) void ARMv4::DataWrite32S(u32 addr, u32 val) { + if (NDS.ARM7BytesToWrite && (addr >> 24) == 0x02) NDS.DecreaseARM7BytesToWrite(4); addr &= ~3; BusWrite32(addr, val); diff --git a/src/ARMInterpreter.cpp b/src/ARMInterpreter.cpp index a5726e5514..a9ca9d708f 100644 --- a/src/ARMInterpreter.cpp +++ b/src/ARMInterpreter.cpp @@ -266,6 +266,10 @@ void A_MRC(ARM* cpu) void A_SVC(ARM* cpu) { + // update ARM9 starvation status on a BIOS call + if (cpu->Num == 1) + cpu->NDS.UpdateARM9Starve((cpu->CurInstr >> 16) & 0xFF, cpu->R[1], cpu->R[2]); + u32 oldcpsr = cpu->CPSR; cpu->CPSR &= ~0xBF; cpu->CPSR |= 0x93; @@ -278,6 +282,9 @@ void A_SVC(ARM* cpu) void T_SVC(ARM* cpu) { + if (cpu->Num == 1) + cpu->NDS.UpdateARM9Starve(cpu->CurInstr & 0xFF, cpu->R[1], cpu->R[2]); + u32 oldcpsr = cpu->CPSR; cpu->CPSR &= ~0xBF; cpu->CPSR |= 0x93; diff --git a/src/NDS.cpp b/src/NDS.cpp index c6dc85fa80..79e51539f0 100644 --- a/src/NDS.cpp +++ b/src/NDS.cpp @@ -508,6 +508,8 @@ void NDS::Reset() IPCFIFO9.Clear(); IPCFIFO7.Clear(); + ARM7BytesToWrite = 0; + DivCnt = 0; SqrtCnt = 0; @@ -1002,6 +1004,11 @@ u32 NDS::RunFrame() dsi.RunNDMAs(0); } } + else if (CPUStop & CPUStop_ARM9BusStarve) + { + // starved of main RAM by the ARM7, see UpdateARM9Starve() + ARM9Timestamp = ARM9Target; + } else { ARM9.Execute(); @@ -1092,6 +1099,46 @@ u32 NDS::RunFrame() } } +void NDS::UpdateARM9Starve(u32 swi, u32 dst, u32 cnt) +{ + // Update the number of bytes that ARM7 will transfer to main RAM. + // DecreaseARM7BytesToWrite() will consumes it as the ARM7 writes; + // at zero the ARM9 is released. + + // accept only CpuSet and CpuFastSet call + // see: https://problemkaputt.de/gbatek-bios-memory-copy.htm + if (swi != 0x0B && swi != 0x0C) return; + + // destination must be in main RAM + if ((dst >> 24) != 0x02) return; + + // bus starvation occurs only when ARM7 has higher priority + if (!(ExMemCnt[0] & (1<<15))) return; + + u32 units = cnt & 0x1FFFFF; + u32 bytes = (swi == 0x0C || (cnt & (1<<26))) ? (units << 2) : (units << 1); + + // The original intention of simulating bus starvation is for some problematic games, + // like DSi PictoChat, whose ARM7 fills a wide range of main RAM. Transfers below 4KB + // are too short for the starvation to matter and are ignored. + if (bytes < 0x1000) return; + + ARM7BytesToWrite = bytes; + CPUStop |= CPUStop_ARM9BusStarve; +} + +void NDS::DecreaseARM7BytesToWrite(u32 bytes) +{ + if (bytes < ARM7BytesToWrite) + { + ARM7BytesToWrite -= bytes; + return; + } + + ARM7BytesToWrite = 0; + CPUStop &= ~CPUStop_ARM9BusStarve; +} + void NDS::Reschedule(u64 target) { if (CurCPU == 0) diff --git a/src/NDS.h b/src/NDS.h index 729706bd9a..b054372359 100644 --- a/src/NDS.h +++ b/src/NDS.h @@ -171,6 +171,8 @@ enum CPUStop_NDMA9_3 = (1<<7), CPUStop_DMA9 = 0xFFF, + CPUStop_ARM9BusStarve = (1<<14), + CPUStop_DMA7_0 = (1<<16), CPUStop_DMA7_1 = (1<<17), CPUStop_DMA7_2 = (1<<18), @@ -289,6 +291,7 @@ class NDS u16 PowerControl9; u16 ExMemCnt[2]; + u32 ARM7BytesToWrite; protected: // These BIOS arrays should be declared *before* the component objects (JIT, SPI, etc.) @@ -443,6 +446,11 @@ class NDS bool HaltInterrupted(u32 cpu) const; void StopCPU(u32 cpu, u32 mask); void ResumeCPU(u32 cpu, u32 mask); + + // ARM9 starvation while the ARM7 has main RAM priority + void UpdateARM9Starve(u32 swi, u32 dst, u32 cnt); + void DecreaseARM7BytesToWrite(u32 bytes); + void GXFIFOStall(); void GXFIFOUnstall(); From c1783da1ae05538e7f25b817f63dbdb087fa3d32 Mon Sep 17 00:00:00 2001 From: Takumi Sueda Date: Mon, 7 Sep 2026 21:27:39 +0900 Subject: [PATCH 2/2] Save the ARM9 starvation state in savestates CPUStop_ARM9BusStarve is saved with CPUStop, but ARM7BytesToWrite, the write count that releases it, was not. A state saved while the ARM9 is starved would leave it stopped forever after loading. Save the count too. This changes the layout of the NDS section, so bump the savestate major version. --- src/NDS.cpp | 1 + src/Savestate.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NDS.cpp b/src/NDS.cpp index 79e51539f0..24bf199e06 100644 --- a/src/NDS.cpp +++ b/src/NDS.cpp @@ -685,6 +685,7 @@ bool NDS::DoSavestate(Savestate* file) file->Var16(&SqrtCnt); file->Var32(&CPUStop); + file->Var32(&ARM7BytesToWrite); for (int i = 0; i < 8; i++) { diff --git a/src/Savestate.h b/src/Savestate.h index 776785f3ac..6c9ba7c22c 100644 --- a/src/Savestate.h +++ b/src/Savestate.h @@ -24,7 +24,7 @@ #include #include "types.h" -#define SAVESTATE_MAJOR 14 +#define SAVESTATE_MAJOR 15 #define SAVESTATE_MINOR 0 // bitmask for the savestate config word