From 82c58527c32945e9371bb48cb696423f0e5f07bd Mon Sep 17 00:00:00 2001 From: Tim Allen Date: Fri, 10 Mar 2017 21:23:29 +1100 Subject: [PATCH] Update to v102r17 release. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit byuu says: Changelog: - GBA: process audio at 2MHz instead of 32KHz¹ - MD: do not allow the 68K to stop the Z80, unless it has been granted bus access first - MD: do not reset bus requested/granted signals when the 68K resets the Z80 - the above two fix The Lost Vikings - MD: clean up the bus address decoding to be more readable - MD: add support for a13000-a130ff (#TIME) region; pass to cartridge I/O² - MD: emulate SRAM mapping used by >16mbit games; bank mapping used by >32mbit games³ - MD: add 'reset pending' flag so that loading save states won't reload 68K PC, SP registers - this fixes save state support ... mostly⁴ - MD: if DMA is not enabled, do not allow CD5 to be set [Cydrak] - this fixes in-game graphics for Ristar. Title screen still corrupted on first run - MD: detect and break sprite lists that form an infinite loop [Cydrak] - this fixes the emulator from dead-locking on certain games - MD: add DC offset to sign DAC PCM samples [Cydrak] - this improves audio in Sonic 3 - MD: 68K TAS has a hardware bug that prevents writing the result back to RAM - this fixes Gargoyles - MD: 68K TRAP should not change CPU interrupt level - this fixes Shining Force II, Shining in the Darkness, etc - icarus: better SRAM heuristics for Mega Drive games Todo: - need to serialize the new cartridge ramEnable, ramWritable, bank variables ¹: so technically, the GBA has its FIFO queue (raw PCM), plus a GB chipset. The GB audio runs at 2MHz. However, I was being lazy and running the sequencer 64 times in a row, thus decimating the audio to 32KHz. But simply discarding 63 out of every 64 samples resorts in muddier sound with more static in it. However ... increasing the audio thread processing intensity 64-fold, and requiring heavy-duty three-chain lowpass and highpass filters is not cheap. For this bump in sound quality, we're eating a loss of about 30% of previous performance. Also note that the GB audio emulation in the GBA core still lacks many of the improvements made to the GB core. I was hoping to complete the GB enhancements, but it seems like I'm never going to pass blargg's psychotic edge case tests. So, first I want to clean up the GB audio to my current coding standards, and then I'll port that over to the GBA, which should further increase sound quality. At that point, it sound exceed mGBA's audio quality (due to the ridiculously high sampling rate and strong-attenuation audio filtering.) ²: word writes are probably not handled correctly ... but games are only supposed to do byte writes here. ³: the SRAM mapping is used by games like "Story of Thor" and "Phantasy Star IV." Unfortunately, the former wasn't released in the US and is region protected. So you'll need to change the NTSU to NTSCJ in md/system/system.cpp in order to boot it. But it does work nicely now. The write protection bit is cleared in the game, and then it fails to write to SRAM (soooooooo many games with SRAM write protection do this), so for now I've had to disable checking that bit. Phantasy Star IV has a US release, but sadly the game doesn't boot yet. Hitting some other bug. The bank mapping is pretty much just for the 40mbit Super Street Fighter game. It shows the Sega and Capcom logos now, but is hitting yet another bug and deadlocking. For now, I emulate the SRAM/bank mapping registers on all cartridges, and set sane defaults. So long as games don't write to $a130XX, they should all continue to work. But obviously, we need to get to a point where higan/icarus can selectively enable these registers on a per-game basis. ⁴: so, the Mega Drive has various ways to lock a chip until another chip releases it. The VDP can lock the 68K, the 68K can lock the Z80, etc. If this happens when you save a state, it'll dead-lock the emulator. So that's obviously a problem that needs to be fixed. The fix will be nasty ... basically, bypassing the dead-lock, creating a miniature, one-instruction-long race condition. Extremely unlikely to cause any issues in practice (it's only a little worse than the SNES CPU/SMP desync), but ... there's nothing I can do about it. So you'll have to take it or leave it. But yeah, for now, save states may lock up the emulator. I need to add code to break the loops when in the process of creating a save state still. --- higan/emulator/emulator.hpp | 4 +- higan/gba/apu/apu.cpp | 11 ++-- higan/md/apu/apu.cpp | 18 +++--- higan/md/apu/apu.hpp | 1 + higan/md/bus/bus.cpp | 82 ++++++++++++++------------- higan/md/cartridge/cartridge.cpp | 24 +++++++- higan/md/cartridge/cartridge.hpp | 7 +++ higan/md/cpu/cpu.cpp | 19 +++---- higan/md/cpu/cpu.hpp | 2 +- higan/md/vdp/io.cpp | 1 + higan/md/vdp/sprite.cpp | 3 +- higan/md/ym2612/ym2612.cpp | 2 +- higan/processor/m68k/instructions.cpp | 10 +++- icarus/heuristics/mega-drive.cpp | 23 +++++++- 14 files changed, 132 insertions(+), 75 deletions(-) diff --git a/higan/emulator/emulator.hpp b/higan/emulator/emulator.hpp index f1f9b27b..c6b85b07 100644 --- a/higan/emulator/emulator.hpp +++ b/higan/emulator/emulator.hpp @@ -12,13 +12,13 @@ using namespace nall; namespace Emulator { static const string Name = "higan"; - static const string Version = "102.16"; + static const string Version = "102.17"; static const string Author = "byuu"; static const string License = "GPLv3"; static const string Website = "http://byuu.org/"; //incremented only when serialization format changes - static const string SerializerVersion = "101"; + static const string SerializerVersion = "102.17"; namespace Constants { namespace Colorburst { diff --git a/higan/gba/apu/apu.cpp b/higan/gba/apu/apu.cpp index 07fce221..4759afa9 100644 --- a/higan/gba/apu/apu.cpp +++ b/higan/gba/apu/apu.cpp @@ -18,9 +18,7 @@ auto APU::Enter() -> void { } auto APU::main() -> void { - for(auto n : range(64)) { - runsequencer(); - } + runsequencer(); int lsample = regs.bias.level - 0x0200; int rsample = regs.bias.level - 0x0200; @@ -64,7 +62,7 @@ auto APU::main() -> void { if(cpu.regs.mode == CPU::Registers::Mode::Stop) lsample = 0, rsample = 0; stream->sample(sclamp<16>(lsample << 6) / 32768.0, sclamp<16>(rsample << 6) / 32768.0); //should be <<5; use <<6 for added volume - step(512); + step(8); } auto APU::step(uint clocks) -> void { @@ -74,8 +72,9 @@ auto APU::step(uint clocks) -> void { auto APU::power() -> void { create(APU::Enter, 16'777'216); - stream = Emulator::audio.createStream(2, frequency() / 512.0); - //todo: run sequencer at higher frequency; add low-pass filter + stream = Emulator::audio.createStream(2, frequency() / 8.0); + stream->addLowPassFilter(20000.0, 3); + stream->addHighPassFilter(20.0, 3); square1.power(); square2.power(); diff --git a/higan/md/apu/apu.cpp b/higan/md/apu/apu.cpp index 229ace32..6192dde7 100644 --- a/higan/md/apu/apu.cpp +++ b/higan/md/apu/apu.cpp @@ -10,7 +10,9 @@ auto APU::Enter() -> void { } auto APU::main() -> void { - if(!state.enabled) return step(1); + if(!state.enabled) { + return step(1); + } if(state.nmiLine) { state.nmiLine = 0; //edge-sensitive @@ -39,7 +41,9 @@ auto APU::setINT(bool value) -> void { } auto APU::enable(bool value) -> void { - if(state.enabled && !value) power(); + //68K cannot disable the Z80 without bus access + if(!bus->granted() && !value) return; + if(state.enabled && !value) reset(); state.enabled = value; } @@ -48,12 +52,12 @@ auto APU::power() -> void { Z80::power(); create(APU::Enter, system.colorburst()); memory::fill(&state, sizeof(State)); +} - r.pc = 0x0000; - r.im = 0; - r.iff1 = 0; - r.iff2 = 0; - r.ir = {}; +auto APU::reset() -> void { + create(APU::Enter, system.colorburst()); + memory::fill(&r, sizeof(Registers)); + memory::fill(&state, sizeof(State)); } } diff --git a/higan/md/apu/apu.hpp b/higan/md/apu/apu.hpp index 5e65765d..1f685250 100644 --- a/higan/md/apu/apu.hpp +++ b/higan/md/apu/apu.hpp @@ -7,6 +7,7 @@ struct APU : Processor::Z80, Thread { auto enable(bool) -> void; auto power() -> void; + auto reset() -> void; auto setNMI(bool value) -> void; auto setINT(bool value) -> void; diff --git a/higan/md/bus/bus.cpp b/higan/md/bus/bus.cpp index 19e90443..1145fdc4 100644 --- a/higan/md/bus/bus.cpp +++ b/higan/md/bus/bus.cpp @@ -7,53 +7,59 @@ BusAPU busAPU; #include "serialization.cpp" auto BusCPU::readByte(uint24 addr) -> uint16 { - if(addr < 0x400000) return cartridge.read(addr & ~1).byte(!addr.bit(0)); - if(addr < 0xa00000) return 0x0000; - if(addr < 0xa10000) return busAPU.granted() ? busAPU.read(addr) : (uint8)0x0000; - if(addr < 0xa11000) return readIO(addr & ~0xff00); - if(addr < 0xa12000) return readIO(addr & ~0x00ff); - if(addr < 0xc00000) return 0x0000; - if(addr < 0xe00000) return vdp.read(addr & ~1).byte(!addr.bit(0)); - return ram[addr & 0xffff]; + if(addr >= 0x000000 && addr <= 0x3fffff) return cartridge.read(addr & ~1).byte(!addr.bit(0)); + if(addr >= 0xa00000 && addr <= 0xa0ffff) return busAPU.granted() ? busAPU.read(addr) : (uint8)0x0000; + if(addr >= 0xa10000 && addr <= 0xa10fff) return readIO(addr & ~0xff00); + if(addr >= 0xa11000 && addr <= 0xa11fff) return readIO(addr & ~0x00ff); + if(addr >= 0xa13000 && addr <= 0xa130ff) return cartridge.readIO(addr); + if(addr >= 0xc00000 && addr <= 0xdfffff) return vdp.read(addr & ~1).byte(!addr.bit(0)); + if(addr >= 0xe00000 && addr <= 0xffffff) { + return ram[addr & 0xffff]; + } + return 0x0000; } auto BusCPU::readWord(uint24 addr) -> uint16 { - if(addr < 0x400000) return cartridge.read(addr); - if(addr < 0xa00000) return 0x0000; - if(addr < 0xa10000) return busAPU.granted() ? busAPU.read(addr) : (uint8)0x0000; - if(addr < 0xa11000) return readIO(addr & ~0xff00) << 0; - if(addr < 0xa12000) return readIO(addr & ~0x00ff) << 8; - if(addr < 0xc00000) return 0x0000; - if(addr < 0xe00000) return vdp.read(addr); - uint16 data = ram[addr + 0 & 0xffff] << 8; - return data | ram[addr + 1 & 0xffff] << 0; + if(addr >= 0x000000 && addr <= 0x3fffff) return cartridge.read(addr); + if(addr >= 0xa00000 && addr <= 0xa0ffff) return busAPU.granted() ? busAPU.read(addr) : (uint8)0x0000; + if(addr >= 0xa10000 && addr <= 0xa10fff) return readIO(addr & ~0xff00) << 0; + if(addr >= 0xa11000 && addr <= 0xa11fff) return readIO(addr & ~0x00ff) << 8; + if(addr >= 0xa13000 && addr <= 0xa130ff) return cartridge.readIO(addr); + if(addr >= 0xc00000 && addr <= 0xdfffff) return vdp.read(addr); + if(addr >= 0xe00000 && addr <= 0xffffff) { + uint16 data = ram[addr + 0 & 0xffff] << 8; + return data | ram[addr + 1 & 0xffff] << 0; + } + return 0x0000; } auto BusCPU::writeByte(uint24 addr, uint16 data) -> void { - if(addr < 0x400000) return cartridge.write(addr & ~1, data << 8 | data << 0); - if(addr < 0xa00000) return; - if(addr < 0xa10000) return busAPU.granted() ? busAPU.write(addr, data) : (void)0; - if(addr < 0xa11000) return writeIO(addr & ~0xff00, data); - if(addr < 0xa12000) return writeIO(addr & ~0x00ff, data); - if(addr < 0xc00000) return; - if(addr < 0xc00010) return vdp.write(addr & ~1, data << 8 | data << 0); - if(addr < 0xc00018) return psg.write(data); - if(addr < 0xe00000) return; - ram[addr & 0xffff] = data; + if(addr >= 0x000000 && addr <= 0x3fffff) return cartridge.write(addr & ~1, data << 8 | data << 0); + if(addr >= 0xa00000 && addr <= 0xa0ffff) return busAPU.granted() ? busAPU.write(addr, data) : (void)0; + if(addr >= 0xa10000 && addr <= 0xa10fff) return writeIO(addr & ~0xff00, data); + if(addr >= 0xa11000 && addr <= 0xa11fff) return writeIO(addr & ~0x00ff, data); + if(addr >= 0xa13000 && addr <= 0xa130ff) return cartridge.writeIO(addr, data); + if(addr >= 0xc00000 && addr <= 0xc0000f) return vdp.write(addr & ~1, data << 8 | data << 0); + if(addr >= 0xc00010 && addr <= 0xc00017) return psg.write(data); + if(addr >= 0xe00000 && addr <= 0xffffff) { + ram[addr & 0xffff] = data; + return; + } } auto BusCPU::writeWord(uint24 addr, uint16 data) -> void { - if(addr < 0x400000) return cartridge.write(addr, data); - if(addr < 0xa00000) return; - if(addr < 0xa10000) return busAPU.granted() ? busAPU.write(addr, data) : (void)0; - if(addr < 0xa11000) return writeIO(addr & ~0xff00, data >> 0); - if(addr < 0xa12000) return writeIO(addr & ~0x00ff, data >> 8); - if(addr < 0xc00000) return; - if(addr < 0xc00010) return vdp.write(addr, data); - if(addr < 0xc00018) return psg.write(data); - if(addr < 0xe00000) return; - ram[addr + 0 & 0xffff] = data >> 8; - ram[addr + 1 & 0xffff] = data >> 0; + if(addr >= 0x000000 && addr <= 0x3fffff) return cartridge.write(addr, data); + if(addr >= 0xa00000 && addr <= 0xa0ffff) return busAPU.granted() ? busAPU.write(addr, data) : (void)0; + if(addr >= 0xa10000 && addr <= 0xa10fff) return writeIO(addr & ~0xff00, data >> 0); + if(addr >= 0xa11000 && addr <= 0xa11fff) return writeIO(addr & ~0x00ff, data >> 8); + if(addr >= 0xa13000 && addr <= 0xa130ff) return cartridge.writeIO(addr, data); + if(addr >= 0xc00000 && addr <= 0xc0000f) return vdp.write(addr, data); + if(addr >= 0xc00010 && addr <= 0xc00017) return psg.write(data); + if(addr >= 0xe00000 && addr <= 0xffffff) { + ram[addr + 0 & 0xffff] = data >> 8; + ram[addr + 1 & 0xffff] = data >> 0; + return; + } } // diff --git a/higan/md/cartridge/cartridge.cpp b/higan/md/cartridge/cartridge.cpp index 0c209e61..480bb489 100644 --- a/higan/md/cartridge/cartridge.cpp +++ b/higan/md/cartridge/cartridge.cpp @@ -66,23 +66,43 @@ auto Cartridge::unload() -> void { } auto Cartridge::power() -> void { + ramEnable = 1; + ramWritable = 1; + for(auto n : range(8)) bank[n] = n; } auto Cartridge::read(uint24 addr) -> uint16 { - if(addr.bit(21) && ram.size) { + if(addr.bit(21) && ram.size && ramEnable) { uint16 data = ram.data[addr + 0 & ram.mask] << 8; return data | ram.data[addr + 1 & ram.mask] << 0; } else { + addr = bank[addr >> 19 & 7] << 19 | (addr & 0x7ffff); uint16 data = rom.data[addr + 0 & rom.mask] << 8; return data | rom.data[addr + 1 & rom.mask] << 0; } } auto Cartridge::write(uint24 addr, uint16 data) -> void { - if(addr.bit(21) && ram.size) { + //emulating RAM write protect bit breaks some commercial software + if(addr.bit(21) && ram.size && ramEnable /* && ramWritable */) { ram.data[addr + 0 & ram.mask] = data >> 8; ram.data[addr + 1 & ram.mask] = data >> 0; } } +auto Cartridge::readIO(uint24 addr) -> uint16 { + return 0x0000; +} + +auto Cartridge::writeIO(uint24 addr, uint16 data) -> void { + if(addr == 0xa130f1) ramEnable = data.bit(0), ramWritable = data.bit(1); + if(addr == 0xa130f3) bank[1] = data; + if(addr == 0xa130f5) bank[2] = data; + if(addr == 0xa130f7) bank[3] = data; + if(addr == 0xa130f9) bank[4] = data; + if(addr == 0xa130fb) bank[5] = data; + if(addr == 0xa130fd) bank[6] = data; + if(addr == 0xa130ff) bank[7] = data; +} + } diff --git a/higan/md/cartridge/cartridge.hpp b/higan/md/cartridge/cartridge.hpp index 9bd3a7b0..23b8cbfe 100644 --- a/higan/md/cartridge/cartridge.hpp +++ b/higan/md/cartridge/cartridge.hpp @@ -12,6 +12,9 @@ struct Cartridge { auto read(uint24 addr) -> uint16; auto write(uint24 addr, uint16 data) -> void; + auto readIO(uint24 addr) -> uint16; + auto writeIO(uint24 addr, uint16 data) -> void; + //serialization.cpp auto serialize(serializer&) -> void; @@ -30,6 +33,10 @@ struct Cartridge { Memory rom; Memory ram; + + uint1 ramEnable; + uint1 ramWritable; + uint6 bank[8]; }; extern Cartridge cartridge; diff --git a/higan/md/cpu/cpu.cpp b/higan/md/cpu/cpu.cpp index 8a7b6b0d..27c4205f 100644 --- a/higan/md/cpu/cpu.cpp +++ b/higan/md/cpu/cpu.cpp @@ -6,23 +6,17 @@ CPU cpu; #include "serialization.cpp" auto CPU::Enter() -> void { - cpu.boot(); while(true) scheduler.synchronize(), cpu.main(); } -auto CPU::boot() -> void { - r.a[7] = bus->readWord(0) << 16 | bus->readWord(2) << 0; - r.pc = bus->readWord(4) << 16 | bus->readWord(6) << 0; -} - auto CPU::main() -> void { - #if 0 - static file fp; - if(!fp) fp.open({Path::user(), "Desktop/tracer.log"}, file::mode::write); - fp.print(pad(disassemble(r.pc), -60, ' '), " ", disassembleRegisters().replace("\n", " "), "\n"); - #endif - if(state.interruptPending) { + if(state.interruptPending.bit((uint)Interrupt::Reset)) { + state.interruptPending.bit((uint)Interrupt::Reset) = 0; + r.a[7] = bus->readWord(0) << 16 | bus->readWord(2) << 0; + r.pc = bus->readWord(4) << 16 | bus->readWord(6) << 0; + } + if(state.interruptPending.bit((uint)Interrupt::HorizontalBlank)) { if(4 > r.i) { state.interruptPending.bit((uint)Interrupt::HorizontalBlank) = 0; @@ -77,6 +71,7 @@ auto CPU::power() -> void { create(CPU::Enter, system.colorburst() * 15.0 / 7.0); memory::fill(&state, sizeof(State)); + state.interruptPending.bit((uint)Interrupt::Reset) = 1; } } diff --git a/higan/md/cpu/cpu.hpp b/higan/md/cpu/cpu.hpp index af78586b..95cd5ae1 100644 --- a/higan/md/cpu/cpu.hpp +++ b/higan/md/cpu/cpu.hpp @@ -2,6 +2,7 @@ struct CPU : Processor::M68K, Thread { enum class Interrupt : uint { + Reset, HorizontalBlank, VerticalBlank, }; @@ -9,7 +10,6 @@ struct CPU : Processor::M68K, Thread { using Thread::synchronize; static auto Enter() -> void; - auto boot() -> void; auto main() -> void; auto step(uint clocks) -> void override; auto synchronize() -> void; diff --git a/higan/md/vdp/io.cpp b/higan/md/vdp/io.cpp index 298e8550..d25c230d 100644 --- a/higan/md/vdp/io.cpp +++ b/higan/md/vdp/io.cpp @@ -130,6 +130,7 @@ auto VDP::writeControlPort(uint16 data) -> void { io.command.bits(2,5) = data.bits(4,7); io.address.bits(14,15) = data.bits(0,1); + if(!dma.io.enable) io.command.bit(5) = 0; if(dma.io.mode == 3) dma.io.wait = false; return; } diff --git a/higan/md/vdp/sprite.cpp b/higan/md/vdp/sprite.cpp index e7d575b9..ca7cf459 100644 --- a/higan/md/vdp/sprite.cpp +++ b/higan/md/vdp/sprite.cpp @@ -38,6 +38,7 @@ auto VDP::Sprite::scanline(uint y) -> void { uint7 link = 0; uint tiles = 0; + uint count = 0; do { auto& object = oam[link]; link = object.link; @@ -48,7 +49,7 @@ auto VDP::Sprite::scanline(uint y) -> void { objects.append(object); tiles += object.width >> 3; - } while(link && link < 80 && objects.size() < 20 && tiles < 40); + } while(link && link < 80 && objects.size() < 20 && tiles < 40 && ++count < 80); } auto VDP::Sprite::run(uint x, uint y) -> void { diff --git a/higan/md/ym2612/ym2612.cpp b/higan/md/ym2612/ym2612.cpp index a880e857..da9ee4de 100644 --- a/higan/md/ym2612/ym2612.cpp +++ b/higan/md/ym2612/ym2612.cpp @@ -139,7 +139,7 @@ auto YM2612::sample() -> void { } int voiceData = sclamp<14>(accumulator) & outMask; - if(dac.enable && (&channel == &channels[5])) voiceData = dac.sample << 6; + if(dac.enable && (&channel == &channels[5])) voiceData = (int)dac.sample - 0x80 << 6; if(channel.leftEnable ) left += voiceData; if(channel.rightEnable) right += voiceData; diff --git a/higan/processor/m68k/instructions.cpp b/higan/processor/m68k/instructions.cpp index e6c1c5fa..9afb359f 100644 --- a/higan/processor/m68k/instructions.cpp +++ b/higan/processor/m68k/instructions.cpp @@ -1121,8 +1121,12 @@ auto M68K::instructionSWAP(DataRegister with) -> void { } auto M68K::instructionTAS(EffectiveAddress with) -> void { - auto data = read(with); - write(with, data | 0x80); +//auto data = read(with); +//write(with, data | 0x80); + + //Mega Drive models 1&2 have a bug that prevents TAS write cycle from completing + //this bugged behavior is required for certain software to function correctly + auto data = read(with); r.c = 0; r.v = 0; @@ -1131,7 +1135,7 @@ auto M68K::instructionTAS(EffectiveAddress with) -> void { } auto M68K::instructionTRAP(uint4 vector) -> void { - exception(Exception::Trap, 32 + vector); + exception(Exception::Trap, 32 + vector, r.i); } auto M68K::instructionTRAPV() -> void { diff --git a/icarus/heuristics/mega-drive.cpp b/icarus/heuristics/mega-drive.cpp index a66fddbc..24baf7c3 100644 --- a/icarus/heuristics/mega-drive.cpp +++ b/icarus/heuristics/mega-drive.cpp @@ -9,10 +9,29 @@ struct MegaDriveCartridge { }; MegaDriveCartridge::MegaDriveCartridge(string location, uint8_t* data, uint size) { + if(size < 0x200) return; + + uint32_t ramFrom = 0; + ramFrom |= data[0x01b4] << 24; + ramFrom |= data[0x01b5] << 16; + ramFrom |= data[0x01b6] << 8; + ramFrom |= data[0x01b7] << 0; + ramFrom &= ~1; //for some reason, most games specify 00200001 as RAM start offset + + uint32_t ramTo = 0; + ramTo |= data[0x01b8] << 24; + ramTo |= data[0x01b9] << 16; + ramTo |= data[0x01ba] << 8; + ramTo |= data[0x01bb] << 0; + + uint32_t ramSize = ramTo - ramFrom; + if(ramSize > 0x020000) ramSize = 0; //sanity check + ramSize = bit::round(ramSize); + manifest.append("board\n"); manifest.append(" rom name=program.rom size=0x", hex(size), "\n"); - if(size <= 0x200000) - manifest.append(" ram name=save.ram size=0x8000\n"); + if(ramSize) + manifest.append(" ram name=save.ram size=0x", hex(ramSize), " offset=0x", hex(ramFrom), "\n"); manifest.append("\n"); manifest.append("information\n"); manifest.append(" title: ", Location::prefix(location), "\n");