diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8306c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Addinational File +LICENSE.meta +README.md.meta +README_CN.md.meta + +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Pp]rojectSettings/ProjectVersion.txt +/Assets/AssetStoreTools* + +# Autogenerated VS/MD solution and project files +ExportedObj/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd + +# Unity3D generated meta files +*.pidb.meta + +# Unity3D Generated File On Crash Reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage \ No newline at end of file diff --git a/Aya.UNes.asmdef b/Aya.UNes.asmdef new file mode 100644 index 0000000..308bc69 --- /dev/null +++ b/Aya.UNes.asmdef @@ -0,0 +1,13 @@ +{ + "name": "Aya.UNes", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Aya.UNes.asmdef.meta b/Aya.UNes.asmdef.meta new file mode 100644 index 0000000..11f4d03 --- /dev/null +++ b/Aya.UNes.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3fe77f1eed9fc0847a86648f644fe815 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..7514485 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# UNes + +本项目修改自 https://github.com/Xyene/Emulator.NES \ No newline at end of file diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..079a930 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ca1eb074eb7f66946b0d974b802d7b8d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Addressable.cs b/Runtime/Addressable.cs new file mode 100644 index 0000000..891a96d --- /dev/null +++ b/Runtime/Addressable.cs @@ -0,0 +1,62 @@ +using System.Runtime.CompilerServices; + +namespace Aya.UNes +{ + public abstract class Addressable + { + public delegate uint ReadDelegate(uint address); + + public delegate void WriteDelegate(uint address, byte val); + + protected readonly Emulator _emulator; + protected readonly ReadDelegate[] _readMap; + protected readonly WriteDelegate[] _writeMap; + protected readonly uint _addressSize; + + protected Addressable(Emulator emulator, uint addressSpace) + { + _emulator = emulator; + _addressSize = addressSpace; + _readMap = new ReadDelegate[addressSpace + 1]; + _writeMap = new WriteDelegate[addressSpace + 1]; + } + + protected virtual void InitializeMemoryMap() + { + _readMap.Fill(address => 0); + + // Some games write to addresses not mapped and expect to continue afterwards + _writeMap.Fill((address, val) => { }); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadByte(uint address) + { + address &= _addressSize; + return _readMap[address](address); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(uint address, uint val) + { + address &= _addressSize; + _writeMap[address](address, (byte)val); + } + + public void MapReadHandler(uint start, uint end, CPU.ReadDelegate func) + { + for (uint i = start; i <= end; i++) + { + _readMap[i] = func; + } + } + + public void MapWriteHandler(uint start, uint end, CPU.WriteDelegate func) + { + for (uint i = start; i <= end; i++) + { + _writeMap[i] = func; + } + } + } +} diff --git a/Runtime/Addressable.cs.meta b/Runtime/Addressable.cs.meta new file mode 100644 index 0000000..84e599c --- /dev/null +++ b/Runtime/Addressable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8649a70e245ed1744aa620cb28504fb5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CPU.Core.cs b/Runtime/CPU.Core.cs new file mode 100644 index 0000000..4785f0d --- /dev/null +++ b/Runtime/CPU.Core.cs @@ -0,0 +1,81 @@ +using System; + +namespace Aya.UNes +{ + sealed partial class CPU + { + public enum InterruptType + { + NMI, + IRQ, + RESET + } + + private readonly uint[] _interruptHandlerOffsets = { 0xFFFA, 0xFFFE, 0xFFFC }; + private readonly bool[] _interrupts = new bool[2]; + + public void Initialize() + { + A = 0; + X = 0; + Y = 0; + SP = 0xFD; + P = 0x24; + + PC = ReadWord(_interruptHandlerOffsets[(int) InterruptType.RESET]); + } + + public void Reset() + { + SP -= 3; + F.InterruptsDisabled = true; + } + + public void TickFromPPU() + { + if (Cycle-- > 0) return; + ExecuteSingleInstruction(); + } + + public void ExecuteSingleInstruction() + { + for (var i = 0; i < _interrupts.Length; i++) + { + if (_interrupts[i]) + { + PushWord(PC); + Push(P); + PC = ReadWord(_interruptHandlerOffsets[i]); + F.InterruptsDisabled = true; + _interrupts[i] = false; + return; + } + } + + _currentInstruction = NextByte(); + + Cycle += _opCodeDefs[_currentInstruction].Cycles; + + ResetInstructionAddressingMode(); + // if (_numExecuted > 10000 && PC - 1 == 0xFF61) + // if(_emulator.Controller.debug || 0x6E00 <= PC && PC <= 0x6EEF) + // Console.WriteLine($"{(PC - 1).ToString("X4")} {_currentInstruction.ToString("X2")} {opcodeNames[_currentInstruction]}\t\t\tA:{A.ToString("X2")} X:{X.ToString("X2")} Y:{Y.ToString("X2")} P:{P.ToString("X2")} SP:{SP.ToString("X2")}"); + + var op = _opCodes[_currentInstruction]; + if (op == null) + { + throw new ArgumentException(_currentInstruction.ToString("X2")); + } + + op(); + } + + public void TriggerInterrupt(InterruptType type) + { + if (!F.InterruptsDisabled || type == InterruptType.NMI) + { + _interrupts[(int)type] = true; + } + } + } +} diff --git a/Runtime/CPU.Core.cs.meta b/Runtime/CPU.Core.cs.meta new file mode 100644 index 0000000..5f375dc --- /dev/null +++ b/Runtime/CPU.Core.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63ec605962bce0e4899b19d49d4ed747 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CPU.IORegisters.cs b/Runtime/CPU.IORegisters.cs new file mode 100644 index 0000000..0fa4785 --- /dev/null +++ b/Runtime/CPU.IORegisters.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Aya.UNes +{ + sealed partial class CPU + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteIoRegister(uint reg, byte val) + { + switch (reg) + { + case 0x4014: // OAM DMA + _emulator.PPU.PerformDMA(val); + break; + case 0x4016: + _emulator.Controller.Strobe(val == 1); + break; + } + + if (reg <= 0x401F) + { + return; // APU write + } + + throw new NotImplementedException($"{reg:X4} = {val:X2}"); + } + + public uint ReadIORegister(uint reg) + { + switch (reg) + { + case 0x4016: + return (uint) _emulator.Controller.ReadState() & 0x1; + } + return 0x00; + //throw new NotImplementedException(); + } + } +} diff --git a/Runtime/CPU.IORegisters.cs.meta b/Runtime/CPU.IORegisters.cs.meta new file mode 100644 index 0000000..42ddf45 --- /dev/null +++ b/Runtime/CPU.IORegisters.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6ad8b3c04ef34de48b5cab3029811d9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CPU.Instructions.cs b/Runtime/CPU.Instructions.cs new file mode 100644 index 0000000..459d834 --- /dev/null +++ b/Runtime/CPU.Instructions.cs @@ -0,0 +1,463 @@ +using System; +using static Aya.UNes.CPU.AddressingMode; + +namespace Aya.UNes +{ + public sealed partial class CPU + { + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public class OpCodeDef : Attribute + { + public int OpCode; + public int Cycles = 1; + public bool PageBoundary; + public bool RMW; + public AddressingMode Mode = None; + } + + [OpCodeDef(OpCode = 0x20, Cycles = 6)] + private void JSR() + { + PushWord(PC + 1); + PC = NextWord(); + } + + [OpCodeDef(OpCode = 0x40, Cycles = 6)] + private void RTI() + { + // TODO: this dummy fetch should happen for all single-byte instructions + NextByte(); + P = Pop(); + PC = PopWord(); + } + + [OpCodeDef(OpCode = 0x60, Cycles = 6)] + private void RTS() + { + NextByte(); + PC = PopWord() + 1; + } + + [OpCodeDef(OpCode = 0xC8, Cycles = 2)] + private void INY() => Y++; + + [OpCodeDef(OpCode = 0x88, Cycles = 2)] + private void DEY() => Y--; + + [OpCodeDef(OpCode = 0xE8, Cycles = 2)] + private void INX() => X++; + + [OpCodeDef(OpCode = 0xCA, Cycles = 2, RMW = true)] + private void DEX() => X--; + + [OpCodeDef(OpCode = 0xA8, Cycles = 2)] + private void TAY() => Y = A; + + [OpCodeDef(OpCode = 0x98, Cycles = 2)] + private void TYA() => A = Y; + + [OpCodeDef(OpCode = 0xAA, Cycles = 2, RMW = true)] + private void TAX() => X = A; + + [OpCodeDef(OpCode = 0x8A, Cycles = 2, RMW = true)] + private void TXA() => A = X; + + [OpCodeDef(OpCode = 0xBA, Cycles = 2)] + private void TSX() => X = SP; + + [OpCodeDef(OpCode = 0x9A, Cycles = 2, RMW = true)] + private void TXS() => SP = X; + + [OpCodeDef(OpCode = 0x08, Cycles = 3)] + private void PHP() => Push(P | BreakSourceBit); + + [OpCodeDef(OpCode = 0x28, Cycles = 4)] + private void PLP() => P = (uint)(Pop() & ~BreakSourceBit); + + [OpCodeDef(OpCode = 0x68, Cycles = 4)] + private void PLA() => A = Pop(); + + [OpCodeDef(OpCode = 0x48, Cycles = 3)] + private void PHA() => Push(A); + + [OpCodeDef(OpCode = 0x24, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x2C, Mode = Absolute, Cycles = 4)] + private void BIT() + { + uint val = AddressRead(); + F.Overflow = (val & 0x40) > 0; + F.Zero = (val & A) == 0; + F.Negative = (val & 0x80) > 0; + } + + private void Branch(bool cond) + { + uint nPC = (uint)(PC + NextSByte() + 1); + if (cond) + { + PC = nPC; + Cycle++; + } + } + + [OpCodeDef(OpCode = 0x4C, Cycles = 3)] + [OpCodeDef(OpCode = 0x6C, Cycles = 5)] + private void JMP() + { + if (_currentInstruction == 0x4C) + { + PC = NextWord(); + } + else if (_currentInstruction == 0x6C) + { + uint off = NextWord(); + // AN INDIRECT JUMP MUST NEVER USE A VECTOR BEGINNING ON THE LAST BYTE OF A PAGE + // + // If address $3000 contains $40, $30FF contains $80, and $3100 contains $50, + // the result of JMP ($30FF) will be a transfer of control to $4080 rather than + // $5080 as you intended i.e. the 6502 took the low byte of the address from + // $30FF and the high byte from $3000. + // + // http://www.6502.org/tutorials/6502opcodes.html + uint hi = (off & 0xFF) == 0xFF ? off - 0xFF : off + 1; + uint oldPC = PC; + PC = ReadByte(off) | (ReadByte(hi) << 8); + + if ((oldPC & 0xFF00) != (PC & 0xFF00)) + { + Cycle += 2; + } + } + else + { + throw new NotImplementedException(); + } + } + + [OpCodeDef(OpCode = 0xB0, Cycles = 2)] + private void BCS() => Branch(F.Carry); + + [OpCodeDef(OpCode = 0x90, Cycles = 2)] + private void BCC() => Branch(!F.Carry); + + [OpCodeDef(OpCode = 0xF0, Cycles = 2)] + private void BEQ() => Branch(F.Zero); + + [OpCodeDef(OpCode = 0xD0, Cycles = 2)] + private void BNE() => Branch(!F.Zero); + + [OpCodeDef(OpCode = 0x70, Cycles = 2)] + private void BVS() => Branch(F.Overflow); + + [OpCodeDef(OpCode = 0x50, Cycles = 2)] + private void BVC() => Branch(!F.Overflow); + + [OpCodeDef(OpCode = 0x10, Cycles = 2)] + private void BPL() => Branch(!F.Negative); + + [OpCodeDef(OpCode = 0x30, Cycles = 2)] + private void BMI() => Branch(F.Negative); + + [OpCodeDef(OpCode = 0x81, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0x91, Mode = IndirectY, Cycles = 6)] + [OpCodeDef(OpCode = 0x95, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0x99, Mode = AbsoluteY, Cycles = 5)] + [OpCodeDef(OpCode = 0x9D, Mode = AbsoluteX, Cycles = 5)] + [OpCodeDef(OpCode = 0x85, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x8D, Mode = Absolute, Cycles = 4)] + private void STA() => AddressWrite(A); + + [OpCodeDef(OpCode = 0x96, Mode = ZeroPageY, Cycles = 4)] + [OpCodeDef(OpCode = 0x86, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x8E, Mode = Absolute, Cycles = 4)] + private void STX() => AddressWrite(X); + + [OpCodeDef(OpCode = 0x94, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0x84, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x8C, Mode = Absolute, Cycles = 4)] + private void STY() => AddressWrite(Y); + + [OpCodeDef(OpCode = 0x18, Cycles = 2)] + private void CLC() => F.Carry = false; + + [OpCodeDef(OpCode = 0x38, Cycles = 2)] + private void SEC() => F.Carry = true; + + [OpCodeDef(OpCode = 0x58, Cycles = 2)] + private void CLI() => F.InterruptsDisabled = false; + + [OpCodeDef(OpCode = 0x78, Cycles = 2)] + private void SEI() => F.InterruptsDisabled = true; + + [OpCodeDef(OpCode = 0xB8, Cycles = 2)] + private void CLV() => F.Overflow = false; + + [OpCodeDef(OpCode = 0xD8, Cycles = 2)] + private void CLD() => F.DecimalMode = false; + + [OpCodeDef(OpCode = 0xF8, Cycles = 2)] + private void SED() => F.DecimalMode = true; + + [OpCodeDef(OpCode = 0xEA, Cycles = 2)] + [OpCodeDef(OpCode = 0x1A, Cycles = 2)] // Unofficial + [OpCodeDef(OpCode = 0x3A, Cycles = 2)] // Unofficial + [OpCodeDef(OpCode = 0x5A, Cycles = 2)] // Unofficial + [OpCodeDef(OpCode = 0x7A, Cycles = 2)] // Unofficial + [OpCodeDef(OpCode = 0xDA, Cycles = 2)] // Unofficial + [OpCodeDef(OpCode = 0xFA, Cycles = 2)] // Unofficial + private void NOP() { } + + [OpCodeDef(OpCode = 0xA1, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0xA5, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0xA9, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0xAD, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0xB1, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0xB5, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0xB9, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0xBD, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void LDA() => A = AddressRead(); + + [OpCodeDef(OpCode = 0xA0, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0xA4, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0xAC, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0xB4, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0xBC, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void LDY() => Y = AddressRead(); + + [OpCodeDef(OpCode = 0xA2, Mode = Immediate, Cycles = 2, RMW = true)] + [OpCodeDef(OpCode = 0xA6, Mode = ZeroPage, Cycles = 3, RMW = true)] + [OpCodeDef(OpCode = 0xAE, Mode = Absolute, Cycles = 4, RMW = true)] + [OpCodeDef(OpCode = 0xB6, Mode = ZeroPageY, Cycles = 4, RMW = true)] + [OpCodeDef(OpCode = 0xBE, Mode = AbsoluteY, Cycles = 4, PageBoundary = true, RMW = true)] + private void LDX() => X = AddressRead(); + + [OpCodeDef(OpCode = 0x01, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0x05, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x09, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0x0D, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0x11, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0x15, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0x19, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0x1D, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void ORA() => A |= AddressRead(); + + [OpCodeDef(OpCode = 0x21, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0x25, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x29, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0x2D, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0x31, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0x35, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0x39, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0x3D, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void AND() => A &= AddressRead(); + + [OpCodeDef(OpCode = 0x41, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0x45, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x49, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0x4D, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0x51, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0x55, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0x59, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0x5D, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void EOR() => A ^= AddressRead(); + + [OpCodeDef(OpCode = 0xE1, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0xE5, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x69, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0xE9, Mode = Immediate, Cycles = 2)] // Official duplicate of $69 + [OpCodeDef(OpCode = 0xEB, Mode = Immediate, Cycles = 2)] // Unofficial duplicate of $69 + [OpCodeDef(OpCode = 0xED, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0xF1, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0xF5, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0xF9, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0xFD, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void SBC() => ADCImpl((byte)~AddressRead()); + + [OpCodeDef(OpCode = 0x61, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0x65, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0x69, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0x6D, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0x71, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0x75, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0x79, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0x7D, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void ADC() => ADCImpl(AddressRead()); + + private void ADCImpl(uint val) + { + int nA = (sbyte)A + (sbyte)val + (sbyte)(F.Carry ? 1 : 0); + F.Overflow = nA < -128 || nA > 127; + F.Carry = (A + val + (F.Carry ? 1 : 0)) > 0xFF; + A = (byte)(nA & 0xFF); + } + + [OpCodeDef(OpCode = 0x00, Cycles = 7)] + private void BRK() + { + NextByte(); + Push(P | BreakSourceBit); + F.InterruptsDisabled = true; + PC = ReadByte(0xFFFE) | (ReadByte(0xFFFF) << 8); + } + + [OpCodeDef(OpCode = 0xC1, Mode = IndirectX, Cycles = 6)] + [OpCodeDef(OpCode = 0xC5, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0xC9, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0xCD, Mode = Absolute, Cycles = 4)] + [OpCodeDef(OpCode = 0xD1, Mode = IndirectY, Cycles = 5, PageBoundary = true)] + [OpCodeDef(OpCode = 0xD5, Mode = ZeroPageX, Cycles = 4)] + [OpCodeDef(OpCode = 0xD9, Mode = AbsoluteY, Cycles = 4, PageBoundary = true)] + [OpCodeDef(OpCode = 0xDD, Mode = AbsoluteX, Cycles = 4, PageBoundary = true)] + private void CMP() => CMPImpl(A); + + [OpCodeDef(OpCode = 0xE0, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0xE4, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0xEC, Mode = Absolute, Cycles = 4)] + private void CPX() => CMPImpl(X); + + [OpCodeDef(OpCode = 0xC0, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0xC4, Mode = ZeroPage, Cycles = 3)] + [OpCodeDef(OpCode = 0xCC, Mode = Absolute, Cycles = 4)] + private void CPY() => CMPImpl(Y); + + private void CMPImpl(uint reg) + { + long d = reg - (int)AddressRead(); + + F.Negative = (d & 0x80) > 0 && d != 0; + F.Carry = d >= 0; + F.Zero = d == 0; + } + + [OpCodeDef(OpCode = 0x46, Mode = ZeroPage, Cycles = 5, RMW = true)] + [OpCodeDef(OpCode = 0x4E, Mode = Absolute, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x56, Mode = ZeroPageX, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x5E, Mode = AbsoluteX, Cycles = 7, RMW = true)] + [OpCodeDef(OpCode = 0x4A, Mode = Direct, Cycles = 2, RMW = true)] + private void LSR() + { + uint D = AddressRead(); + F.Carry = (D & 0x1) > 0; + D >>= 1; + _F(D); + AddressWrite(D); + } + + [OpCodeDef(OpCode = 0x06, Mode = ZeroPage, Cycles = 5, RMW = true)] + [OpCodeDef(OpCode = 0x0E, Mode = Absolute, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x16, Mode = ZeroPageX, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x1E, Mode = AbsoluteX, Cycles = 7, RMW = true)] + [OpCodeDef(OpCode = 0x0A, Mode = Direct, Cycles = 2, RMW = true)] + private void ASL() + { + uint D = AddressRead(); + F.Carry = (D & 0x80) > 0; + D <<= 1; + _F(D); + AddressWrite(D); + } + + [OpCodeDef(OpCode = 0x66, Mode = ZeroPage, Cycles = 5, RMW = true)] + [OpCodeDef(OpCode = 0x6E, Mode = Absolute, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x76, Mode = ZeroPageX, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x7E, Mode = AbsoluteX, Cycles = 7, RMW = true)] + [OpCodeDef(OpCode = 0x6A, Mode = Direct, Cycles = 2, RMW = true)] + private void ROR() + { + uint D = AddressRead(); + bool c = F.Carry; + F.Carry = (D & 0x1) > 0; + D >>= 1; + if (c) D |= 0x80; + _F(D); + AddressWrite(D); + } + + [OpCodeDef(OpCode = 0x26, Mode = ZeroPage, Cycles = 5, RMW = true)] + [OpCodeDef(OpCode = 0x2E, Mode = Absolute, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x36, Mode = ZeroPageX, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0x3E, Mode = AbsoluteX, Cycles = 7, RMW = true)] + [OpCodeDef(OpCode = 0x2A, Mode = Direct, Cycles = 2, RMW = true)] + private void ROL() + { + uint D = AddressRead(); + bool c = F.Carry; + F.Carry = (D & 0x80) > 0; + D <<= 1; + if (c) D |= 0x1; + _F(D); + AddressWrite(D); + } + + [OpCodeDef(OpCode = 0xE6, Mode = ZeroPage, Cycles = 5, RMW = true)] + [OpCodeDef(OpCode = 0xEE, Mode = Absolute, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0xF6, Mode = ZeroPageX, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0xFE, Mode = AbsoluteX, Cycles = 7, RMW = true)] + private void INC() + { + byte D = (byte)(AddressRead() + 1); + _F(D); + AddressWrite(D); + } + + [OpCodeDef(OpCode = 0xC6, Mode = ZeroPage, Cycles = 5, RMW = true)] + [OpCodeDef(OpCode = 0xCE, Mode = Absolute, Cycles = 3, RMW = true)] + [OpCodeDef(OpCode = 0xD6, Mode = ZeroPageX, Cycles = 6, RMW = true)] + [OpCodeDef(OpCode = 0xDE, Mode = AbsoluteX, Cycles = 7, RMW = true)] + private void DEC() + { + byte D = (byte)(AddressRead() - 1); + _F(D); + AddressWrite(D); + } + + #region Unofficial Opcodes + + [OpCodeDef(OpCode = 0x80, Cycles = 2)] + [OpCodeDef(OpCode = 0x82, Cycles = 2)] + [OpCodeDef(OpCode = 0x89, Cycles = 2)] + [OpCodeDef(OpCode = 0xC2, Cycles = 2)] + [OpCodeDef(OpCode = 0xE2, Cycles = 2)] + private void SKB() => NextByte(); // Essentially a 2-byte NOP + + [OpCodeDef(OpCode = 0x0B, Mode = Immediate, Cycles = 2)] + [OpCodeDef(OpCode = 0x2B, Mode = Immediate, Cycles = 2)] + private void ANC() + { + A &= AddressRead(); + F.Carry = F.Negative; + } + + [OpCodeDef(OpCode = 0x4B, Mode = Immediate, Cycles = 2)] + private void ALR() + { + A &= AddressRead(); + F.Carry = (A & 0x1) > 0; + A >>= 1; + _F(A); + } + + [OpCodeDef(OpCode = 0x6B, Mode = Immediate, Cycles = 2)] + private void ARR() + { + A &= AddressRead(); + bool c = F.Carry; + F.Carry = (A & 0x1) > 0; + A >>= 1; + if (c) A |= 0x80; + _F(A); + } + + [OpCodeDef(OpCode = 0xAB, Mode = Immediate, Cycles = 2)] + private void ATX() + { + // This opcode ORs the A register with #$EE, ANDs the result with an immediate + // value, and then stores the result in both A and X. + A |= ReadByte(0xEE); + A &= AddressRead(); + X = A; + } + + #endregion + } +} diff --git a/Runtime/CPU.Instructions.cs.meta b/Runtime/CPU.Instructions.cs.meta new file mode 100644 index 0000000..7817694 --- /dev/null +++ b/Runtime/CPU.Instructions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cae8846694d75f04a8214a484032d533 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CPU.Memory.cs b/Runtime/CPU.Memory.cs new file mode 100644 index 0000000..7c3291e --- /dev/null +++ b/Runtime/CPU.Memory.cs @@ -0,0 +1,165 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using static Aya.UNes.CPU.AddressingMode; + +namespace Aya.UNes +{ + sealed partial class CPU + { + public enum AddressingMode + { + None, + Direct, + Immediate, + ZeroPage, + Absolute, + ZeroPageX, + ZeroPageY, + AbsoluteX, + AbsoluteY, + IndirectX, + IndirectY + } + + private uint? _currentMemoryAddress; + private uint _rmwValue; + + private void ResetInstructionAddressingMode() => _currentMemoryAddress = null; + + private uint _Address() + { + var def = _opCodeDefs[_currentInstruction]; + switch (def.Mode) + { + case Immediate: + return PC++; + case ZeroPage: + return NextByte(); + case Absolute: + return NextWord(); + case ZeroPageX: + return (NextByte() + X) & 0xFF; + case ZeroPageY: + return (NextByte() + Y) & 0xFF; + case AbsoluteX: + var address = NextWord(); + if (def.PageBoundary && (address & 0xFF00) != ((address + X) & 0xFF00)) + { + Cycle += 1; + } + + return address + X; + case AbsoluteY: + address = NextWord(); + if (def.PageBoundary && (address & 0xFF00) != ((address + Y) & 0xFF00)) + { + Cycle += 1; + } + + return address + Y; + case IndirectX: + var off = (NextByte() + X) & 0xFF; + return ReadByte(off) | (ReadByte((off + 1) & 0xFF) << 8); + case IndirectY: + off = NextByte() & 0xFF; + address = ReadByte(off) | (ReadByte((off + 1) & 0xFF) << 8); + if (def.PageBoundary && (address & 0xFF00) != ((address + Y) & 0xFF00)) + { + Cycle += 1; + } + + return (address + Y) & 0xFFFF; + } + throw new NotImplementedException(); + } + + public uint AddressRead() + { + if (_opCodeDefs[_currentInstruction].Mode == Direct) + { + return _rmwValue = A; + } + + if (_currentMemoryAddress == null) + { + _currentMemoryAddress = _Address(); + } + + return _rmwValue = ReadByte((uint)_currentMemoryAddress) & 0xFF; + } + + public void AddressWrite(uint val) + { + if (_opCodeDefs[_currentInstruction].Mode == Direct) + { + A = val; + } + else + { + if (_currentMemoryAddress == null) + { + _currentMemoryAddress = _Address(); + } + + if (_opCodeDefs[_currentInstruction].RMW) + { + WriteByte((uint)_currentMemoryAddress, _rmwValue); + } + + WriteByte((uint)_currentMemoryAddress, val); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint ReadWord(uint address) => ReadByte(address) | (ReadByte(address + 1) << 8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint NextByte() => ReadByte(PC++); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint NextWord() => NextByte() | (NextByte() << 8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private sbyte NextSByte() => (sbyte)NextByte(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Push(uint what) + { + WriteByte(0x100 + SP, what); + SP--; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint Pop() + { + SP++; + return ReadByte(0x100 + SP); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PushWord(uint what) + { + Push(what >> 8); + Push(what & 0xFF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint PopWord() => Pop() | (Pop() << 8); + + protected override void InitializeMemoryMap() + { + base.InitializeMemoryMap(); + + MapReadHandler(0x0000, 0x1FFF, address => _ram[address & 0x07FF]); + MapReadHandler(0x2000, 0x3FFF, address => _emulator.PPU.ReadRegister((address & 0x7) - 0x2000)); + MapReadHandler(0x4000, 0x4017, ReadIORegister); + + MapWriteHandler(0x0000, 0x1FFF, (address, val) => _ram[address & 0x07FF] = val); + MapWriteHandler(0x2000, 0x3FFF, (address, val) => _emulator.PPU.WriteRegister((address & 0x7) - 0x2000, val)); + MapWriteHandler(0x4000, 0x401F, WriteIoRegister); + + _emulator.Mapper.InitializeMemoryMap(this); + } + } +} diff --git a/Runtime/CPU.Memory.cs.meta b/Runtime/CPU.Memory.cs.meta new file mode 100644 index 0000000..e10a02a --- /dev/null +++ b/Runtime/CPU.Memory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc18416f50415b74e96092e235a95f92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CPU.Registers.cs b/Runtime/CPU.Registers.cs new file mode 100644 index 0000000..dfe05fb --- /dev/null +++ b/Runtime/CPU.Registers.cs @@ -0,0 +1,89 @@ +using System.Runtime.CompilerServices; + +namespace Aya.UNes +{ + sealed partial class CPU + { + private const int CarryBit = 0x1; + private const int ZeroBit = 0x2; + private const int InterruptDisabledBit = 0x4; + private const int DecimalModeBit = 0x8; + private const int BreakSourceBit = 0x10; + private const int OverflowBit = 0x40; + private const int NegativeBit = 0x80; + + public class CPUFlags + { + public bool Negative; + public bool Overflow; + public bool BreakSource; + public bool DecimalMode; + public bool InterruptsDisabled; + public bool Zero; + public bool Carry; + } + + public readonly CPUFlags F = new CPUFlags(); + + public uint _A, _X, _Y, _SP; + public uint PC; + + public uint A + { + get => _A; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set => _A = _F(value & 0xFF); + } + + public uint X + { + get => _X; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set => _X = _F(value & 0xFF); + } + + public uint Y + { + get => _Y; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set => _Y = _F(value & 0xFF); + } + + public uint SP + { + get => _SP; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set => _SP = value & 0xFF; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint _F(uint val) + { + F.Zero = (val & 0xFF) == 0; + F.Negative = (val & 0x80) > 0; + return val; + } + + public uint P + { + get => (uint) ((F.Carry.AsByte() << 0) | + (F.Zero.AsByte() << 1) | + (F.InterruptsDisabled.AsByte() << 2) | + (F.DecimalMode.AsByte() << 3) | + (F.BreakSource.AsByte() << 4) | + (1 << 5) | + (F.Overflow.AsByte() << 6) | + (F.Negative.AsByte() << 7)); + set + { + F.Carry = (value & CarryBit) > 0; + F.Zero = (value & ZeroBit) > 0; + F.InterruptsDisabled = (value & InterruptDisabledBit) > 0; + F.DecimalMode = (value & DecimalModeBit) > 0; + F.BreakSource = (value & BreakSourceBit) > 0; + F.Overflow = (value & OverflowBit) > 0; + F.Negative = (value & NegativeBit) > 0; + } + } + } +} diff --git a/Runtime/CPU.Registers.cs.meta b/Runtime/CPU.Registers.cs.meta new file mode 100644 index 0000000..508a0d9 --- /dev/null +++ b/Runtime/CPU.Registers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57750595bb8e0d24babf00d51e4624bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CPU.cs b/Runtime/CPU.cs new file mode 100644 index 0000000..ad35697 --- /dev/null +++ b/Runtime/CPU.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Aya.UNes +{ + sealed partial class CPU : Addressable + { + private readonly byte[] _ram = new byte[0x800]; + public int Cycle; + private uint _currentInstruction; + + public delegate void OpCode(); + + private readonly OpCode[] _opCodes = new OpCode[256]; + private readonly string[] _opCodeNames = new string[256]; + private readonly OpCodeDef[] _opCodeDefs = new OpCodeDef[256]; + + public CPU(Emulator emulator) : base(emulator, 0xFFFF) + { + InitializeOpCodes(); + InitializeMemoryMap(); + Initialize(); + } + + private void InitializeOpCodes() + { + var opCodeBindings = from opCode in GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + let defs = opCode.GetCustomAttributes(typeof(OpCodeDef), false) + where defs.Length > 0 + select new + { + binding = (OpCode)Delegate.CreateDelegate(typeof(OpCode), this, opCode.Name), + name = opCode.Name, + defs = (from d in defs select (OpCodeDef)d) + }; + + foreach (var opCode in opCodeBindings) + { + foreach (var def in opCode.defs) + { + _opCodes[def.OpCode] = opCode.binding; + _opCodeNames[def.OpCode] = opCode.name; + _opCodeDefs[def.OpCode] = def; + } + } + } + + public void Execute() + { + for (var i = 0; i < 5000; i++) + { + ExecuteSingleInstruction(); + } + + uint w; + ushort x = 6000; + string z = ""; + while ((w = ReadByte(x)) != '\0') + { + z += (char)w; + } + } + } +} diff --git a/Runtime/CPU.cs.meta b/Runtime/CPU.cs.meta new file mode 100644 index 0000000..f80ca2b --- /dev/null +++ b/Runtime/CPU.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa724afa42498dd4e80f44c3da2a5881 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Cartridge.cs b/Runtime/Cartridge.cs new file mode 100644 index 0000000..74f4e66 --- /dev/null +++ b/Runtime/Cartridge.cs @@ -0,0 +1,66 @@ +using System; + +namespace Aya.UNes +{ + public class Cartridge + { + public readonly byte[] Raw; + public readonly int PRGROMSize; + public readonly int CHRROMSize; + public readonly int PRGRAMSize; + public readonly int PRGROMOffset; + public readonly int MapperNumber; + public readonly byte[] PRGROM; + public readonly byte[] CHRROM; + public VRAMMirroringMode MirroringMode; + + public enum VRAMMirroringMode + { + Horizontal, Vertical, All, Upper, Lower + } + + public Cartridge(byte[] bytes) + { + Raw = bytes; + + var header = BitConverter.ToInt32(Raw, 0); + if (header != 0x1A53454E) // "NES" + { + throw new FormatException("unexpected header value " + header.ToString("X")); + } + + PRGROMSize = Raw[4] * 0x4000; // 16kb units + CHRROMSize = Raw[5] * 0x2000; // 8kb units + PRGRAMSize = Raw[8] * 0x2000; + + var hasTrainer = (Raw[6] & 0b100) > 0; + PRGROMOffset = 16 + (hasTrainer ? 512 : 0); + + MirroringMode = (Raw[6] & 0x1) > 0 ? VRAMMirroringMode.Vertical : VRAMMirroringMode.Horizontal; + if ((Raw[6] & 0x8) > 0) + { + MirroringMode = VRAMMirroringMode.All; + } + + MapperNumber = (Raw[6] >> 4) | (Raw[7] & 0xF0); + + PRGROM = new byte[PRGROMSize]; + Array.Copy(Raw, PRGROMOffset, PRGROM, 0, PRGROMSize); + + if (CHRROMSize == 0) + { + CHRROM = new byte[0x2000]; + } + else + { + CHRROM = new byte[CHRROMSize]; + Array.Copy(Raw, PRGROMOffset + PRGROMSize, CHRROM, 0, CHRROMSize); + } + } + + public override string ToString() + { + return $"Cartridge{{PRGROMSize={PRGROMSize}, CHRROMSize={CHRROMSize}, PRGROMOffset={PRGROMOffset}, MapperNumber={MapperNumber}}}"; + } + } +} diff --git a/Runtime/Cartridge.cs.meta b/Runtime/Cartridge.cs.meta new file mode 100644 index 0000000..4fcccd7 --- /dev/null +++ b/Runtime/Cartridge.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b45675095efbb224cad44ee16c2587ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Controller.meta b/Runtime/Controller.meta new file mode 100644 index 0000000..52daa48 --- /dev/null +++ b/Runtime/Controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 87f4900ceb2e3b24caa8eec8df6b34e7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Controller/IController.cs b/Runtime/Controller/IController.cs new file mode 100644 index 0000000..6cff890 --- /dev/null +++ b/Runtime/Controller/IController.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace Aya.UNes.Controller +{ + public interface IController + { + void Strobe(bool on); + + int ReadState(); + + void PressKey(KeyCode keyCode); + + void ReleaseKey(KeyCode keyCode); + } +} diff --git a/Runtime/Controller/IController.cs.meta b/Runtime/Controller/IController.cs.meta new file mode 100644 index 0000000..097e178 --- /dev/null +++ b/Runtime/Controller/IController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fde4f0c3566240b4fa824b147b019ff7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Controller/NesController.cs b/Runtime/Controller/NesController.cs new file mode 100644 index 0000000..4c89a87 --- /dev/null +++ b/Runtime/Controller/NesController.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Aya.UNes.Controller +{ + public class NesController : IController + { + private int _data; + private int _serialData; + private bool _strobing; + + public bool Debug; + // bit: 7 6 5 4 3 2 1 0 + // button: A B Select Start Up Down Left + + private readonly Dictionary _keyMapping = new Dictionary + { + {KeyCode.A, 7}, + {KeyCode.S, 6}, + {KeyCode.RightShift, 5}, + {KeyCode.Return, 4}, + {KeyCode.UpArrow, 3}, + {KeyCode.DownArrow, 2}, + {KeyCode.LeftArrow, 1}, + {KeyCode.RightArrow, 0}, + }; + + public void Strobe(bool on) + { + _serialData = _data; + _strobing = on; + } + + public int ReadState() + { + int ret = ((_serialData & 0x80) > 0).AsByte(); + if (!_strobing) + { + _serialData <<= 1; + _serialData &= 0xFF; + } + + return ret; + } + + public void PressKey(KeyCode keyCode) + { + if (keyCode == KeyCode.P) + { + Debug ^= true; + } + + if (!_keyMapping.ContainsKey(keyCode)) + { + return; + } + + _data |= 1 << _keyMapping[keyCode]; + } + + public void ReleaseKey(KeyCode keyCode) + { + if (!_keyMapping.ContainsKey(keyCode)) + { + return; + } + + _data &= ~(1 << _keyMapping[keyCode]); + } + } +} diff --git a/Runtime/Controller/NesController.cs.meta b/Runtime/Controller/NesController.cs.meta new file mode 100644 index 0000000..0582652 --- /dev/null +++ b/Runtime/Controller/NesController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a6001b2b26e33144b77fe0b02e21dc0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Emulator.cs b/Runtime/Emulator.cs new file mode 100644 index 0000000..5bae444 --- /dev/null +++ b/Runtime/Emulator.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Aya.UNes.Controller; +using Aya.UNes.Mapper; + +namespace Aya.UNes +{ + public class Emulator + { + private static readonly Dictionary> Mappers = (from type in Assembly.GetExecutingAssembly().GetTypes() + let def = (MapperDef)type.GetCustomAttributes(typeof(MapperDef), true).FirstOrDefault() + where def != null + select new { def, type }).ToDictionary(a => a.def.Id, a => new KeyValuePair(a.type, a.def)); + + public IController Controller; + + public readonly CPU CPU; + + public readonly PPU PPU; + + public readonly BaseMapper Mapper; + + public readonly Cartridge Cartridge; + + public Emulator(byte[] bytes, IController controller) + { + Cartridge = new Cartridge(bytes); + if (!Mappers.ContainsKey(Cartridge.MapperNumber)) + { + throw new NotImplementedException($"unsupported mapper {Cartridge.MapperNumber}"); + } + + Mapper = (BaseMapper)Activator.CreateInstance(Mappers[Cartridge.MapperNumber].Key, this); + CPU = new CPU(this); + PPU = new PPU(this); + Controller = controller; + + // Load(); + } + + public void Save(string path) + { + using (var fs = new FileStream(path + ".sav", FileMode.Create, FileAccess.Write)) + { + Mapper.Save(fs); + } + } + + public void Load(string path) + { + var sav = path + ".sav"; + if (!File.Exists(sav)) + { + return; + } + + using (var fs = new FileStream(sav, FileMode.Open, FileAccess.Read)) + { + Mapper.Load(fs); + } + } + } +} diff --git a/Runtime/Emulator.cs.meta b/Runtime/Emulator.cs.meta new file mode 100644 index 0000000..49774f0 --- /dev/null +++ b/Runtime/Emulator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c56d6518f6443745a10dd7dda0ac7f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper.meta b/Runtime/Mapper.meta new file mode 100644 index 0000000..f05ed2d --- /dev/null +++ b/Runtime/Mapper.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d22acc98b0a7fa042bb19b9894899fb7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/AxROM.cs b/Runtime/Mapper/AxROM.cs new file mode 100644 index 0000000..2541c27 --- /dev/null +++ b/Runtime/Mapper/AxROM.cs @@ -0,0 +1,26 @@ +using static Aya.UNes.Cartridge.VRAMMirroringMode; + +namespace Aya.UNes.Mapper +{ + [MapperDef(7)] + public class AxROM : BaseMapper + { + protected int _bankOffset; + private readonly Cartridge.VRAMMirroringMode[] _mirroringModes = { Lower, Upper }; + + public AxROM(Emulator emulator) : base(emulator) + { + _emulator.Cartridge.MirroringMode = _mirroringModes[0]; + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_bankOffset + (address - 0x8000)]); + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => + { + _bankOffset = (val & 0x7) * 0x8000; + _emulator.Cartridge.MirroringMode = _mirroringModes[(val >> 4) & 0x1]; + }); + } + } +} diff --git a/Runtime/Mapper/AxROM.cs.meta b/Runtime/Mapper/AxROM.cs.meta new file mode 100644 index 0000000..67ab997 --- /dev/null +++ b/Runtime/Mapper/AxROM.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 632b907c0f30abb47b218545e40ff06d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/BaseMapper.cs b/Runtime/Mapper/BaseMapper.cs new file mode 100644 index 0000000..e8d0abf --- /dev/null +++ b/Runtime/Mapper/BaseMapper.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; + +namespace Aya.UNes.Mapper +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class MapperDef : Attribute + { + public int Id; + public string Name; + public string Description; + + public MapperDef() + { + + } + + public MapperDef(int id) + { + Id = id; + } + } + + public abstract class BaseMapper + { + protected readonly Emulator _emulator; + protected readonly byte[] _prgROM; + protected readonly byte[] _prgRAM = new byte[0x2000]; + protected readonly byte[] _chrROM; + protected readonly uint _lastBankOffset; + + protected BaseMapper(Emulator emulator) + { + _emulator = emulator; + var cart = emulator.Cartridge; + _prgROM = cart.PRGROM; + _chrROM = cart.CHRROM; + _lastBankOffset = (uint) _prgROM.Length - 0x4000; + } + + public virtual void InitializeMemoryMap(CPU cpu) + { + + } + + public virtual void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[address]); + ppu.MapWriteHandler(0x0000, 0x1FFF, (address, val) => _chrROM[address] = val); + } + + public virtual void ProcessCycle(int scanLine, int cycle) + { + + } + + public virtual void Save(Stream os) + { + os.Write(_prgRAM, 0, _prgRAM.Length); + } + + public virtual void Load(Stream os) + { + using (var binaryReader = new BinaryReader(os)) + { + var ram = binaryReader.ReadBytes((int)os.Length); + Array.Copy(ram, _prgRAM, ram.Length); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Mapper/BaseMapper.cs.meta b/Runtime/Mapper/BaseMapper.cs.meta new file mode 100644 index 0000000..6e44d0d --- /dev/null +++ b/Runtime/Mapper/BaseMapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a468573a1ef266542a055638aeef33cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/CNROM.cs b/Runtime/Mapper/CNROM.cs new file mode 100644 index 0000000..029eb97 --- /dev/null +++ b/Runtime/Mapper/CNROM.cs @@ -0,0 +1,32 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(3)] + public class CNROM : BaseMapper + { + protected int _bankOffset; + + public CNROM(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_bankOffset + address]); + } + + public override void InitializeMemoryMap(CPU cpu) + { + if (_prgROM.Length == 0x8000) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[address - 0x8000]); + } + else + { + cpu.MapReadHandler(0x8000, 0xBFFF, address => _prgROM[address - 0x8000]); + cpu.MapReadHandler(0xC000, 0xFFFF, address => _prgROM[address - 0xC000]); + } + + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => _bankOffset = (val & 0x3) * 0x2000); + } + } +} diff --git a/Runtime/Mapper/CNROM.cs.meta b/Runtime/Mapper/CNROM.cs.meta new file mode 100644 index 0000000..15d040c --- /dev/null +++ b/Runtime/Mapper/CNROM.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fdaa43625af49e54aacf69d2ed92544f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/Camerica.cs b/Runtime/Mapper/Camerica.cs new file mode 100644 index 0000000..6a9ad0c --- /dev/null +++ b/Runtime/Mapper/Camerica.cs @@ -0,0 +1,27 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(71)] + public class Camerica : BaseMapper + { + protected int _prgBankOffset; + + public Camerica(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xBFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + cpu.MapReadHandler(0xC000, 0xFFFF, address => _prgROM[_prgROM.Length - 0x4000 + (address - 0xC000)]); + + // Actually starts at 0x8000, but use 0x9000 for compatibility w/o submapper + cpu.MapWriteHandler(0x9000, 0x9FFF, (address, val) => + { + // TODO: Fire Hawk mirroring + }); + + // The number of bits available vary: 4 for the BF9093, 3 for the BF9097, and 2 for the BF9096. + cpu.MapWriteHandler(0xC000, 0xFFFF, (address, val) => _prgBankOffset = (val & 0xF) * 0x4000 % _prgROM.Length); + } + } +} diff --git a/Runtime/Mapper/Camerica.cs.meta b/Runtime/Mapper/Camerica.cs.meta new file mode 100644 index 0000000..2d735de --- /dev/null +++ b/Runtime/Mapper/Camerica.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a879b12017bd97949a00317043b96f54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/ColorDreams.cs b/Runtime/Mapper/ColorDreams.cs new file mode 100644 index 0000000..57f10f8 --- /dev/null +++ b/Runtime/Mapper/ColorDreams.cs @@ -0,0 +1,29 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(11)] + public class ColorDreams : BaseMapper + { + protected int _prgBankOffset; + protected int _chrBankOffset; + + public ColorDreams(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffset + address]); + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => + { + _prgBankOffset = (val & 0x3) * 0x8000; + _chrBankOffset = (val >> 4) * 0x2000; + }); + } + } +} diff --git a/Runtime/Mapper/ColorDreams.cs.meta b/Runtime/Mapper/ColorDreams.cs.meta new file mode 100644 index 0000000..172a6aa --- /dev/null +++ b/Runtime/Mapper/ColorDreams.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50a4c8ff195db574585c43ba079d8195 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/DxROM.cs b/Runtime/Mapper/DxROM.cs new file mode 100644 index 0000000..0d6d17d --- /dev/null +++ b/Runtime/Mapper/DxROM.cs @@ -0,0 +1,39 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(206)] + public class DxROM : MMC3 + { + public DxROM(Emulator emulator) : base(emulator) + { + _prgBankingMode = PRGBankingMode.SwitchFix; + _chrBankingMode = CHRBankingMode.TwoFour; + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffsets[(address - 0x8000) / 0x2000] + address % 0x2000]); + + cpu.MapWriteHandler(0x8000, 0x9FFF, (address, val) => + { + if ((address & 0x1) == 0) + { + _currentBank = val & 0x7u; + } + else + { + if (_currentBank <= 1) val &= 0x1F; + else if (_currentBank <= 5) val &= 0x3F; + else val &= 0xF; + + _banks[_currentBank] = val; + UpdateOffsets(); + } + }); + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffsets[address / 0x400] + address % 0x400]); + } + } +} diff --git a/Runtime/Mapper/DxROM.cs.meta b/Runtime/Mapper/DxROM.cs.meta new file mode 100644 index 0000000..dd2b1c3 --- /dev/null +++ b/Runtime/Mapper/DxROM.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8ae98e170ab70143bc7284dc01b9627 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/GxROM.cs b/Runtime/Mapper/GxROM.cs new file mode 100644 index 0000000..c435678 --- /dev/null +++ b/Runtime/Mapper/GxROM.cs @@ -0,0 +1,29 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(66)] + public class GxROM : BaseMapper + { + protected int _prgBankOffset; + protected int _chrBankOffset; + + public GxROM(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffset + address]); + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => + { + _prgBankOffset = ((val >> 4) & 0x3) * 0x8000; + _chrBankOffset = (val & 0x3) * 0x2000; + }); + } + } +} diff --git a/Runtime/Mapper/GxROM.cs.meta b/Runtime/Mapper/GxROM.cs.meta new file mode 100644 index 0000000..2810704 --- /dev/null +++ b/Runtime/Mapper/GxROM.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a06e1a753215b64886b90ecde825cec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/Jaleco.cs b/Runtime/Mapper/Jaleco.cs new file mode 100644 index 0000000..5ccbd2d --- /dev/null +++ b/Runtime/Mapper/Jaleco.cs @@ -0,0 +1,29 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(140)] + public class Jaleco : BaseMapper + { + protected int _prgBankOffset; + protected int _chrBankOffset; + + public Jaleco(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffset + address]); + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + + cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) => + { + _prgBankOffset = ((val >> 4) & 0x3) * 0x8000; + _chrBankOffset = (val & 0x3) * 0x2000; + }); + } + } +} diff --git a/Runtime/Mapper/Jaleco.cs.meta b/Runtime/Mapper/Jaleco.cs.meta new file mode 100644 index 0000000..836c62f --- /dev/null +++ b/Runtime/Mapper/Jaleco.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac2f62d216cf70643919c921900c6e70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/MMC1.cs b/Runtime/Mapper/MMC1.cs new file mode 100644 index 0000000..7360857 --- /dev/null +++ b/Runtime/Mapper/MMC1.cs @@ -0,0 +1,170 @@ +using static Aya.UNes.Cartridge.VRAMMirroringMode; + +namespace Aya.UNes.Mapper +{ + [MapperDef(1)] + public class MMC1 : BaseMapper + { + // TODO: are MMC1 and MMC1A even different chip types? + public enum ChipType { MMC1, MMC1A, MMC1B, MMC1C } + public enum CHRBankingMode { Single, Double } + public enum PRGBankingMode { Switch32Kb, Switch16KbFixFirst, Switch16KbFixLast } + + private readonly Cartridge.VRAMMirroringMode[] _mirroringModes = { Lower, Upper, Vertical, Horizontal }; + + private readonly ChipType _type; + private CHRBankingMode _chrBankingMode; + private PRGBankingMode _prgBankingMode; + + private uint _serialData; + private int _serialPos; + + private uint _control; + + private readonly uint[] _chrBankOffsets = new uint[2]; + private readonly uint[] _chrBanks = new uint[2]; + + private readonly uint[] _prgBankOffsets = new uint[2]; + private uint _prgBank; + + private bool _prgRAMEnabled; + + private uint? _lastWritePC; + + public MMC1(Emulator emulator) : this(emulator, ChipType.MMC1B) + { + + } + + public MMC1(Emulator emulator, ChipType chipType) : base(emulator) + { + _type = chipType; + if (chipType == ChipType.MMC1B) _prgRAMEnabled = true; + UpdateControl(0x0F); + _emulator.Cartridge.MirroringMode = Horizontal; + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x6000, 0x7FFF, address => _prgRAM[address - 0x6000]); + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffsets[(address - 0x8000) / 0x4000] + address % 0x4000]); + + cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) => + { + // PRG RAM is always enabled on MMC1A + if (_type == ChipType.MMC1A || _prgRAMEnabled) + _prgRAM[address - 0x6000] = val; + }); + + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => + { + // Explicitly ignore the second write happening on consecutive cycles + // of an RMW instruction + var cycle = _emulator.CPU.PC; + if (cycle == _lastWritePC) + return; + _lastWritePC = cycle; + + if ((val & 0x80) > 0) + { + _serialData = 0; + _serialPos = 0; + UpdateControl(_control | 0x0C); + } + else + { + _serialData |= (uint)((val & 0x1) << _serialPos); + _serialPos++; + + if (_serialPos == 5) + { + // Address is incompletely decoded + address &= 0x6000; + if (address == 0x0000) + UpdateControl(_serialData); + else if (address == 0x2000) + UpdateCHRBank(0, _serialData); + else if (address == 0x4000) + UpdateCHRBank(1, _serialData); + else if (address == 0x6000) + UpdatePRGBank(_serialData); + + _serialData = 0; + _serialPos = 0; + } + } + }); + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffsets[address / 0x1000] + address % 0x1000]); + ppu.MapWriteHandler(0x0000, 0x1FFF, (address, val) => _chrROM[_chrBankOffsets[address / 0x1000] + address % 0x1000] = val); + } + + private void UpdateControl(uint value) + { + _control = value; + + _emulator.Cartridge.MirroringMode = _mirroringModes[value & 0x3]; + + _chrBankingMode = (CHRBankingMode)((value >> 4) & 0x1); + + var prgMode = (value >> 2) & 0x3; + // Both 0 and 1 are 32Kb switch + if (prgMode == 0) prgMode = 1; + _prgBankingMode = (PRGBankingMode)(prgMode - 1); + + UpdateCHRBank(1, _chrBanks[1]); + UpdateCHRBank(0, _chrBanks[0]); + UpdatePRGBank(_prgBank); + } + + private void UpdatePRGBank(uint value) + { + _prgBank = value; + + _prgRAMEnabled = (value & 0x10) == 0; + value &= 0xF; + + switch (_prgBankingMode) + { + case PRGBankingMode.Switch32Kb: + value >>= 1; + value *= 0x4000; + _prgBankOffsets[0] = value; + _prgBankOffsets[1] = value + 0x4000; + break; + case PRGBankingMode.Switch16KbFixFirst: + _prgBankOffsets[0] = 0; + _prgBankOffsets[1] = value * 0x4000; + break; + case PRGBankingMode.Switch16KbFixLast: + _prgBankOffsets[0] = value * 0x4000; + _prgBankOffsets[1] = _lastBankOffset; + break; + } + } + + private void UpdateCHRBank(uint bank, uint value) + { + _chrBanks[bank] = value; + + // TODO FIXME: I feel like this branch should only be taken + // when bank == 0, but this breaks Final Fantasy + // When can banking mode change without UpdateCHRBank being called? + if (_chrBankingMode == CHRBankingMode.Single) + { + value = _chrBanks[0]; + value >>= 1; + value *= 0x1000; + _chrBankOffsets[0] = value; + _chrBankOffsets[1] = value + 0x1000; + } + else + { + _chrBankOffsets[bank] = value * 0x1000; + } + } + } +} diff --git a/Runtime/Mapper/MMC1.cs.meta b/Runtime/Mapper/MMC1.cs.meta new file mode 100644 index 0000000..4c02868 --- /dev/null +++ b/Runtime/Mapper/MMC1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3ab06f7819df254491a7afa6a82bb43 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/MMC2.cs b/Runtime/Mapper/MMC2.cs new file mode 100644 index 0000000..db617a0 --- /dev/null +++ b/Runtime/Mapper/MMC2.cs @@ -0,0 +1,33 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(Id = 9, Description = "Mike Tyson's Punch-Out!!")] + public class MMC2 : MMC4 + { + public MMC2(Emulator emulator) : base(emulator) + { + + } + + public override void InitializeMemoryMap(CPU cpu) + { + base.InitializeMemoryMap(cpu); + + cpu.MapReadHandler(0x8000, 0xBFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + cpu.MapReadHandler(0xA000, 0xFFFF, address => _prgROM[_prgROM.Length - 0x4000 - 0x2000 + (address - 0xA000)]); + + cpu.MapWriteHandler(0xA000, 0xAFFF, (address, val) => _prgBankOffset = (val & 0xF) * 0x2000); + } + + protected override void GetLatch(uint address, out uint latch, out bool? on) + { + base.GetLatch(address, out latch, out on); + + // For MMC2, only 0xFD8 and 0xFE8 trigger the latch, + // not the whole range like in MMC4 + if (latch == 0 && (address & 0x3) != 0) + { + on = null; + } + } + } +} diff --git a/Runtime/Mapper/MMC2.cs.meta b/Runtime/Mapper/MMC2.cs.meta new file mode 100644 index 0000000..3270f4a --- /dev/null +++ b/Runtime/Mapper/MMC2.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 522a51dcf1b92d2498013b4d6f9accb0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/MMC3.cs b/Runtime/Mapper/MMC3.cs new file mode 100644 index 0000000..9e61e97 --- /dev/null +++ b/Runtime/Mapper/MMC3.cs @@ -0,0 +1,182 @@ +using static Aya.UNes.Cartridge.VRAMMirroringMode; + +namespace Aya.UNes.Mapper +{ + [MapperDef(4)] + public class MMC3 : BaseMapper + { + // Different PRG RAM write/enable controls + public enum ChipType { MMC3, MMC6 } + public enum CHRBankingMode { TwoFour, FourTwo } + public enum PRGBankingMode { SwitchFix, FixSwitch } + + private readonly Cartridge.VRAMMirroringMode[] _mirroringModes = { Vertical, Horizontal }; + + private readonly ChipType _type; + protected CHRBankingMode _chrBankingMode; + protected PRGBankingMode _prgBankingMode; + + + protected readonly uint[] _chrBankOffsets = new uint[8]; + protected uint[] _prgBankOffsets; + protected readonly uint[] _banks = new uint[8]; + protected uint _currentBank; + + private uint _irqReloadValue; + private uint _irqCounter; + protected bool _irqEnabled; + + private bool _prgRAMEnabled; + + public MMC3(Emulator emulator) : this(emulator, ChipType.MMC3) + { + + } + + public MMC3(Emulator emulator, ChipType chipType) : base(emulator) + { + _type = chipType; + _prgBankOffsets = new uint[] { 0, 0x2000, _lastBankOffset, _lastBankOffset + 0x2000 }; + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x6000, 0x7FFF, address => _prgRAM[address - 0x6000]); + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffsets[(address - 0x8000) / 0x2000] + address % 0x2000]); + + cpu.MapWriteHandler(0x6000, 0xFFFF, WriteByte); + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffsets[address / 0x400] + address % 0x400]); + ppu.MapWriteHandler(0x0000, 0x1FFF, (address, val) => _chrROM[_chrBankOffsets[address / 0x400] + address % 0x400] = val); + } + + public override void ProcessCycle(int scanLine, int cycle) + { + if (_emulator.PPU.F.RenderingEnabled && cycle == 260 && (0 <= scanLine && scanLine < 240 || scanLine == -1)) + { + if (_irqCounter == 0) + { + _irqCounter = _irqReloadValue; + } + else + { + _irqCounter--; + if (_irqEnabled && _irqCounter == 0) + { + _emulator.CPU.TriggerInterrupt(CPU.InterruptType.IRQ); + } + } + } + } + + protected void WriteByte(uint addr, byte value) + { + bool even = (addr & 0x1) == 0; + + if (addr < 0x8000) + { + if (_prgRAMEnabled) + { + _prgRAM[addr - 0x6000] = value; + } + } + else if (addr < 0xA000) + { + if (even) + { + _currentBank = value & 0x7u; + _prgBankingMode = (PRGBankingMode)((value >> 6) & 0x1); + _chrBankingMode = (CHRBankingMode)((value >> 7) & 0x1); + } + else + { + _banks[_currentBank] = value; + } + + UpdateOffsets(); + } + else if (addr < 0xC000) + { + if (even) + { + _emulator.Cartridge.MirroringMode = _mirroringModes[value & 0x1]; + } + else + { + _prgRAMEnabled = (value & 0xC0) == 0x80; + } + } + else if (addr < 0xE000) + { + if (even) + { + _irqReloadValue = value; + } + else + { + _irqCounter = 0; + } + } + else + { + _irqEnabled = !even; + } + } + + protected void UpdateOffsets() + { + switch (_prgBankingMode) + { + case PRGBankingMode.SwitchFix: + _prgBankOffsets[0] = _banks[6] * 0x2000; + _prgBankOffsets[1] = _banks[7] * 0x2000; + _prgBankOffsets[2] = _lastBankOffset; + _prgBankOffsets[3] = _lastBankOffset + 0x2000; + break; + case PRGBankingMode.FixSwitch: + _prgBankOffsets[0] = _lastBankOffset; + _prgBankOffsets[1] = _banks[7] * 0x2000; + _prgBankOffsets[2] = _banks[6] * 0x2000; + _prgBankOffsets[3] = _lastBankOffset + 0x2000; + break; + } + + switch (_chrBankingMode) + { + case CHRBankingMode.TwoFour: + _chrBankOffsets[0] = _banks[0] & 0xFE; + _chrBankOffsets[1] = _banks[0] | 0x01; + _chrBankOffsets[2] = _banks[1] & 0xFE; + _chrBankOffsets[3] = _banks[1] | 0x01; + _chrBankOffsets[4] = _banks[2]; + _chrBankOffsets[5] = _banks[3]; + _chrBankOffsets[6] = _banks[4]; + _chrBankOffsets[7] = _banks[5]; + break; + case CHRBankingMode.FourTwo: + _chrBankOffsets[0] = _banks[2]; + _chrBankOffsets[1] = _banks[3]; + _chrBankOffsets[2] = _banks[4]; + _chrBankOffsets[3] = _banks[5]; + _chrBankOffsets[4] = _banks[0] & 0xFE; + _chrBankOffsets[5] = _banks[0] | 0x01; + _chrBankOffsets[6] = _banks[1] & 0xFE; + _chrBankOffsets[7] = _banks[1] | 0x01; + break; + } + + for (var i = 0; i < _prgBankOffsets.Length; i++) + { + _prgBankOffsets[i] %= (uint)_prgROM.Length; + } + + for (var i = 0; i < _chrBankOffsets.Length; i++) + { + _chrBankOffsets[i] = (uint) (_chrBankOffsets[i] * 0x400 % _chrROM.Length); + } + } + } +} diff --git a/Runtime/Mapper/MMC3.cs.meta b/Runtime/Mapper/MMC3.cs.meta new file mode 100644 index 0000000..0adfec7 --- /dev/null +++ b/Runtime/Mapper/MMC3.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82fb09fbeba58ae458f58e52898fedb1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/MMC4.cs b/Runtime/Mapper/MMC4.cs new file mode 100644 index 0000000..8c54e4b --- /dev/null +++ b/Runtime/Mapper/MMC4.cs @@ -0,0 +1,73 @@ +using static Aya.UNes.Cartridge.VRAMMirroringMode; + +namespace Aya.UNes.Mapper +{ + [MapperDef(10)] + public class MMC4 : BaseMapper + { + protected readonly Cartridge.VRAMMirroringMode[] _mirroringModes = { Vertical, Horizontal }; + + protected int _prgBankOffset; + protected int[,] _chrBankOffsets = new int[2, 2]; + protected bool[] _latches = new bool[2]; + + public MMC4(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x6000, 0x7FFF, address => _prgRAM[address - 0x6000]); + cpu.MapReadHandler(0x8000, 0xBFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + cpu.MapReadHandler(0xC000, 0xFFFF, address => _prgROM[_prgROM.Length - 0x4000 + (address - 0xC000)]); + + cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) => _prgRAM[address - 0x6000] = val); + cpu.MapWriteHandler(0xA000, 0xAFFF, (address, val) => _prgBankOffset = (val & 0xF) * 0x4000); + cpu.MapWriteHandler(0xB000, 0xEFFF, (address, val) => + { + var bank = (address - 0xB000) / 0x2000; + var latch = ((address & 0x1FFF) == 0).AsByte(); + _chrBankOffsets[bank, latch] = (val & 0x1F) * 0x1000; + }); + + cpu.MapWriteHandler(0xF000, 0xFFFF, (address, val) => _emulator.Cartridge.MirroringMode = _mirroringModes[val & 0x1]); + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => + { + var bank = address / 0x1000; + var ret = _chrROM[_chrBankOffsets[bank, _latches[bank].AsByte()] + address % 0x1000]; + if ((address & 0x08) > 0) + { + GetLatch(address, out uint latch, out bool? on); + + if (on != null) + { + _latches[latch] = (bool)on; + } + } + + return ret; + }); + } + + protected virtual void GetLatch(uint address, out uint latch, out bool? on) + { + latch = (address >> 12) & 0x1; + on = null; + + address = (address >> 4) & 0xFF; + + if (address == 0xFE) + { + on = true; + } + else if (address == 0xFD) + { + on = false; + } + } + } +} diff --git a/Runtime/Mapper/MMC4.cs.meta b/Runtime/Mapper/MMC4.cs.meta new file mode 100644 index 0000000..81f2dfe --- /dev/null +++ b/Runtime/Mapper/MMC4.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54d8f341641616d45b20c65ec4a75ff2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/Mapper094.cs b/Runtime/Mapper/Mapper094.cs new file mode 100644 index 0000000..79c6753 --- /dev/null +++ b/Runtime/Mapper/Mapper094.cs @@ -0,0 +1,17 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(Id = 94, Description = "Senjou no Ookami")] + public class Mapper094 : UxROM + { + public Mapper094(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(CPU cpu) + { + base.InitializeMemoryMap(cpu); + + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => _bankOffset = (val & 0x1C) << 12); + } + } +} diff --git a/Runtime/Mapper/Mapper094.cs.meta b/Runtime/Mapper/Mapper094.cs.meta new file mode 100644 index 0000000..70a9d00 --- /dev/null +++ b/Runtime/Mapper/Mapper094.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0abeb567769f715418ae9125bd029206 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/Mapper155.cs b/Runtime/Mapper/Mapper155.cs new file mode 100644 index 0000000..899f8f2 --- /dev/null +++ b/Runtime/Mapper/Mapper155.cs @@ -0,0 +1,11 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(Id = 155, Description = "MMC1A")] + public class Mapper155 : MMC1 + { + // Mapper for games requiring MMC1A + public Mapper155(Emulator emulator) : base(emulator, ChipType.MMC1A) + { + } + } +} diff --git a/Runtime/Mapper/Mapper155.cs.meta b/Runtime/Mapper/Mapper155.cs.meta new file mode 100644 index 0000000..a775951 --- /dev/null +++ b/Runtime/Mapper/Mapper155.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc90c4e04b12a134c884ca3431375aac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/Mapper180.cs b/Runtime/Mapper/Mapper180.cs new file mode 100644 index 0000000..3e10aaa --- /dev/null +++ b/Runtime/Mapper/Mapper180.cs @@ -0,0 +1,21 @@ +namespace Aya.UNes.Mapper +{ + // Mapper used strictly for Crazy Climber; logic is slighly different + [MapperDef(Id = 180, Description = "Crazy Climber")] + public class Mapper180 : UxROM + { + public Mapper180(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(CPU cpu) + { + base.InitializeMemoryMap(cpu); + + // $8000-$C000 is fixed to *first* bank + cpu.MapReadHandler(0x8000, 0xBFFF, address => _prgROM[address - 0x8000]); + // $C000-$FFFF is switchable, controlled the same as UxROM + cpu.MapReadHandler(0xC000, 0xFFFF, address => _prgROM[_bankOffset + (address - 0xC000)]); + } + } +} diff --git a/Runtime/Mapper/Mapper180.cs.meta b/Runtime/Mapper/Mapper180.cs.meta new file mode 100644 index 0000000..41282e8 --- /dev/null +++ b/Runtime/Mapper/Mapper180.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ed79f8d409c2864bab9bc85848bc36a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/NROM.cs b/Runtime/Mapper/NROM.cs new file mode 100644 index 0000000..e254cc5 --- /dev/null +++ b/Runtime/Mapper/NROM.cs @@ -0,0 +1,23 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(0)] + public class NROM : BaseMapper + { + private readonly byte[] _addressSpace = new byte[0x2000 + 0x8000]; // Space for $2000 VRAM + $8000 PRG + + public NROM(Emulator emulator) : base(emulator) + { + for (var i = 0; i < 0x8000; i++) + { + var offset = _emulator.Cartridge.PRGROMSize == 0x4000 ? i & 0xBFFF : i; + _addressSpace[0x2000 + i] = _prgROM[offset]; + } + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x6000, 0xFFFF, address => _addressSpace[address - 0x6000]); + cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) => _addressSpace[address - 0x6000] = val); + } + } +} diff --git a/Runtime/Mapper/NROM.cs.meta b/Runtime/Mapper/NROM.cs.meta new file mode 100644 index 0000000..f09b0e2 --- /dev/null +++ b/Runtime/Mapper/NROM.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7781217265cd36043acd57aa3a049d25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/Nina003006.cs b/Runtime/Mapper/Nina003006.cs new file mode 100644 index 0000000..9cc9572 --- /dev/null +++ b/Runtime/Mapper/Nina003006.cs @@ -0,0 +1,32 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(79)] + public class Nina003006 : BaseMapper + { + protected int _prgBankOffset; + protected int _chrBankOffset; + + public Nina003006(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(PPU ppu) + { + ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffset + address]); + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffset + (address - 0x8000)]); + + cpu.MapWriteHandler(0x4000, 0x5FFF, (address, val) => + { + if ((address & 0b1110_0001_0000_0000) == 0b0100_0001_0000_0000) + { + _prgBankOffset = ((val >> 4) & 0x3) * 0x8000; + _chrBankOffset = (val & 0x3) * 0x2000; + } + }); + } + } +} \ No newline at end of file diff --git a/Runtime/Mapper/Nina003006.cs.meta b/Runtime/Mapper/Nina003006.cs.meta new file mode 100644 index 0000000..128d6ad --- /dev/null +++ b/Runtime/Mapper/Nina003006.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4a935d078b54a6a4b9434e3478863251 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Mapper/UxROM.cs b/Runtime/Mapper/UxROM.cs new file mode 100644 index 0000000..1bb1a0d --- /dev/null +++ b/Runtime/Mapper/UxROM.cs @@ -0,0 +1,22 @@ +namespace Aya.UNes.Mapper +{ + [MapperDef(2)] + public class UxROM : BaseMapper + { + protected int _bankOffset; + + public UxROM(Emulator emulator) : base(emulator) + { + } + + public override void InitializeMemoryMap(CPU cpu) + { + cpu.MapReadHandler(0x6000, 0x7FFF, address => _prgRAM[address - 0x6000]); + cpu.MapReadHandler(0x8000, 0xBFFF, address => _prgROM[_bankOffset + (address - 0x8000)]); + cpu.MapReadHandler(0xC000, 0xFFFF, address => _prgROM[_prgROM.Length - 0x4000 + (address - 0xC000)]); + + cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) => _prgRAM[address - 0x6000] = val); + cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) => _bankOffset = (val & 0xF) * 0x4000); + } + } +} diff --git a/Runtime/Mapper/UxROM.cs.meta b/Runtime/Mapper/UxROM.cs.meta new file mode 100644 index 0000000..9b0f9dc --- /dev/null +++ b/Runtime/Mapper/UxROM.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9fa6ddb08aec2014fada61a92af067ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/PPU.Core.cs b/Runtime/PPU.Core.cs new file mode 100644 index 0000000..8af48f0 --- /dev/null +++ b/Runtime/PPU.Core.cs @@ -0,0 +1,380 @@ +using System; + +namespace Aya.UNes +{ + partial class PPU + { + private const int GameWidth = 256, GameHeight = 240; + + private uint _bufferPos; + public readonly uint[] RawBitmap = new uint[GameWidth * GameHeight]; + private readonly uint[] _priority = new uint[GameWidth * GameHeight]; + + // TODO: use real chroma/luma decoding + private readonly uint[] _palette = { + 0x7C7C7C, 0x0000FC, 0x0000BC, 0x4428BC, 0x940084, 0xA80020, 0xA81000, 0x881400, + 0x503000, 0x007800, 0x006800, 0x005800, 0x004058, 0x000000, 0x000000, 0x000000, + 0xBCBCBC, 0x0078F8, 0x0058F8, 0x6844FC, 0xD800CC, 0xE40058, 0xF83800, 0xE45C10, + 0xAC7C00, 0x00B800, 0x00A800, 0x00A844, 0x008888, 0x000000, 0x000000, 0x000000, + 0xF8F8F8, 0x3CBCFC, 0x6888FC, 0x9878F8, 0xF878F8, 0xF85898, 0xF87858, 0xFCA044, + 0xF8B800, 0xB8F818, 0x58D854, 0x58F898, 0x00E8D8, 0x787878, 0x000000, 0x000000, + 0xFCFCFC, 0xA4E4FC, 0xB8B8F8, 0xD8B8F8, 0xF8B8F8, 0xF8A4C0, 0xF0D0B0, 0xFCE0A8, + 0xF8D878, 0xD8F878, 0xB8F8B8, 0xB8F8D8, 0x00FCFC, 0xF8D8F8, 0x000000, 0x000000 + }; + + private int _scanLineCount = 261; + private int _cyclesPerLine = 341; + private int _cpuSyncCounter; + private readonly uint[] _scanLineOAM = new uint[8 * 4]; + private readonly bool[] _isSprite0 = new bool[8]; + private int _spriteCount; + + private long _tileShiftRegister; + private uint _currentNameTableByte; + private uint _currentHighTile, _currentLowTile; + private uint _currentColor; + + public void ProcessPixel(int x, int y) + { + ProcessBackgroundForPixel(x, y); + if (F.DrawSprites) + { + ProcessSpritesForPixel(x, y); + } + + if (y != -1) + { + _bufferPos++; + } + } + + private void CountSpritesOnLine(int scanLine) + { + _spriteCount = 0; + var height = F.TallSpritesEnabled ? 16 : 8; + + for (var idx = 0; idx < _oam.Length; idx += 4) + { + var y = _oam[idx] + 1; + if (scanLine >= y && scanLine < y + height) + { + _isSprite0[_spriteCount] = idx == 0; + _scanLineOAM[_spriteCount * 4 + 0] = _oam[idx + 0]; + _scanLineOAM[_spriteCount * 4 + 1] = _oam[idx + 1]; + _scanLineOAM[_spriteCount * 4 + 2] = _oam[idx + 2]; + _scanLineOAM[_spriteCount * 4 + 3] = _oam[idx + 3]; + _spriteCount++; + } + + if (_spriteCount == 8) + { + break; + } + } + } + + private void NextNameTableByte() + { + _currentNameTableByte = ReadByte(0x2000 | (V & 0x0FFF)); + } + + private void NextTileByte(bool hi) + { + var tileIdx = _currentNameTableByte * 16; + var address = F.PatternTableAddress + tileIdx + FineY; + + if (hi) + { + _currentHighTile = ReadByte(address + 8); + } + else + { + _currentLowTile = ReadByte(address); + } + } + + private void NextAttributeByte() + { + // Bless nesdev + var address = 0x23C0 | (V & 0x0C00) | ((V >> 4) & 0x38) | ((V >> 2) & 0x07); + _currentColor = (ReadByte(address) >> (int)((CoarseX & 2) | ((CoarseY & 2) << 1))) & 0x3; + } + + private void ShiftTileRegister() + { + for (var x = 0; x < 8; x++) + { + uint palette = ((_currentHighTile & 0x80) >> 6) | ((_currentLowTile & 0x80) >> 7); + _tileShiftRegister |= (palette + _currentColor * 4) << ((7 - x) * 4); + _currentLowTile <<= 1; + _currentHighTile <<= 1; + } + } + + private void ProcessBackgroundForPixel(int cycle, int scanLine) + { + if (cycle < 8 && !F.DrawLeftBackground || !F.DrawBackground && scanLine != -1) + { + // Maximally sketchy: if current address is in the PPU palette, then it draws that palette entry if rendering is disabled + // Otherwise, it draws $3F00 (universal bg color) + // https://www.romhacking.net/forum/index.php?topic=20554.0 + // Don't know if any game actually uses it, but a test ROM I wrote unexpectedly showed this + // corner case + RawBitmap[_bufferPos] = _palette[ReadByte(0x3F00 + ((F.BusAddress & 0x3F00) == 0x3F00 ? F.BusAddress & 0x001F : 0)) & 0x3F]; + return; + } + + var paletteEntry = (uint)(_tileShiftRegister >> 32 >> (int)((7 - X) * 4)) & 0x0F; + if (paletteEntry % 4 == 0) paletteEntry = 0; + + if (scanLine != -1) + { + _priority[_bufferPos] = paletteEntry; + RawBitmap[_bufferPos] = _palette[ReadByte(0x3F00u + paletteEntry) & 0x3F]; + } + } + + private void ProcessSpritesForPixel(int x, int scanLine) + { + for (var idx = _spriteCount * 4 - 4; idx >= 0; idx -= 4) + { + var spriteX = _scanLineOAM[idx + 3]; + var spriteY = _scanLineOAM[idx] + 1; + + // Don't draw this sprite if... + if (spriteY == 0 || // it's located at y = 0 + spriteY > 239 || // it's located past y = 239 ($EF) + x >= spriteX + 8 || // it's behind the current dot + x < spriteX || // it's ahead of the current dot + x < 8 && !F.DrawLeftSprites) // it's in the clip area, and clipping is enabled + { + continue; + } + + // amusingly enough, the PPU's palette handling is basically identical + // to that of the Gameboy / Gameboy Color, so I've sort of just copy/pasted + // handling code wholesale from my GBC emulator at + // https://github.com/Xyene/Nitrous-Emulator/blob/master/src/main/java/nitrous/lcd/LCD.java#L642 + var tileIdx = _scanLineOAM[idx + 1]; + if (F.TallSpritesEnabled) + { + tileIdx &= ~0x1u; + } + + tileIdx *= 16; + + var attribute = _scanLineOAM[idx + 2] & 0xE3; + + var palette = attribute & 0x3; + var front = (attribute & 0x20) == 0; + var flipX = (attribute & 0x40) > 0; + var flipY = (attribute & 0x80) > 0; + + var px = (int) (x - spriteX); + var line = (int) (scanLine - spriteY); + + var tableBase = F.TallSpritesEnabled ? (_scanLineOAM[idx + 1] & 1) * 0x1000 : F.SpriteTableAddress; + + if (F.TallSpritesEnabled) + { + if (line >= 8) + { + line -= 8; + if (!flipY) + { + tileIdx += 16; + } + + flipY = false; + } + + if (flipY) + { + tileIdx += 16; + } + } + + // here we handle the x and y flipping by tweaking the indices we are accessing + var logicalX = flipX ? 7 - px : px; + var logicalLine = flipY ? 7 - line : line; + + var address = (uint) (tableBase + tileIdx + logicalLine); + + // this looks bad, but it's about as readable as it's going to get + var color = (uint) ( + ( + ( + ( + // fetch upper bit from 2nd bit plane + ReadByte(address + 8) & (0x80 >> logicalX) + ) >> (7 - logicalX) + ) << 1 // this is the upper bit of the color number + ) | + ( + ( + ReadByte(address) & (0x80 >> logicalX) + ) >> (7 - logicalX) + )); // << 0, this is the lower bit of the color number + + if (color > 0) + { + var backgroundPixel = _priority[_bufferPos]; + // Sprite 0 hits... + if (!(!_isSprite0[idx / 4] || // do not occur on not-0 sprite + x < 8 && !F.DrawLeftSprites || // or if left clipping is enabled + backgroundPixel == 0 || // or if bg pixel is transparent + F.Sprite0Hit || // or if it fired this frame already + x == 255)) // or if x is 255, "for an obscure reason related to the pixel pipeline" + { + F.Sprite0Hit = true; + } + + if (F.DrawBackground && (front || backgroundPixel == 0)) + { + if (scanLine != -1) + { + RawBitmap[_bufferPos] = _palette[ReadByte(0x3F10 + palette * 4 + color) & 0x3F]; + } + } + } + } + } + + public void ProcessFrame() + { + RawBitmap.Fill(0u); + _priority.Fill(0u); + _bufferPos = 0; + + for (var i = -1; i < _scanLineCount; i++) + { + ProcessScanLine(i); + } + } + + public void ProcessScanLine(int line) + { + for (var i = 0; i < _cyclesPerLine; i++) + { + ProcessCycle(line, i); + } + } + + private int _cpuClocksSinceVBL; + private int _ppuClocksSinceVBL; + + public void ProcessCycle(int scanLine, int cycle) + { + var visibleCycle = 1 <= cycle && cycle <= 256; + var prefetchCycle = 321 <= cycle && cycle <= 336; + var fetchCycle = visibleCycle || prefetchCycle; + + if (F.VBlankStarted) + { + _ppuClocksSinceVBL++; + } + + if (0 <= scanLine && scanLine < 240 || scanLine == -1) + { + if (visibleCycle) + { + ProcessPixel(cycle - 1, scanLine); + } + + // During pixels 280 through 304 of this scanline, the vertical scroll bits are reloaded TODO: if rendering is enabled. + if (scanLine == -1 && 280 <= cycle && cycle <= 304) + { + ReloadScrollY(); + } + + if (fetchCycle) + { + _tileShiftRegister <<= 4; + + // See https://wiki.nesdev.com/w/images/d/d1/Ntsc_timing.png + // Takes 8 cycles for tile to be read, 2 per "step" + switch (cycle & 7) + { + case 1: // NT + NextNameTableByte(); + break; + case 3: // AT + NextAttributeByte(); + break; + case 5: // Tile low + NextTileByte(false); + break; + case 7: // Tile high + NextTileByte(true); + break; + case 0: // 2nd cycle of tile high fetch + if (cycle == 256) + IncrementScrollY(); + else + IncrementScrollX(); + // Begin rendering a brand new tile + ShiftTileRegister(); + break; + } + } + + if (cycle == 257) + { + ReloadScrollX(); + // 257 - 320 + // The tile data for the sprites on the next scanline are fetched here. + // TODO: stagger this over all the cycles as opposed to only on 257 + CountSpritesOnLine(scanLine + 1); + } + } + + // TODO: this is a hack; VBlank should be cleared on dot 1 of the pre-render line, + // but for some reason we're at 2272-2273 CPU clocks at that time + // (i.e., our PPU timing is off somewhere by 6-9 PPU cycles per frame) + if (F.VBlankStarted && _cpuClocksSinceVBL == 2270) + { + F.VBlankStarted = false; + _cpuClocksSinceVBL = 0; + } + + if (cycle == 1) + { + if (scanLine == 241) + { + F.VBlankStarted = true; + if (F.NMIEnabled) + { + _emulator.CPU.TriggerInterrupt(CPU.InterruptType.NMI); + } + } + + // Happens at the same time as 1st cycle of NT byte fetch + if (scanLine == -1) + { + // Console.WriteLine(_ppuClocksSinceVBL); + _ppuClocksSinceVBL = 0; + F.VBlankStarted = false; + F.Sprite0Hit = false; + F.SpriteOverflow = false; + } + } + + _emulator.Mapper.ProcessCycle(scanLine, cycle); + + if (_cpuSyncCounter + 1 == 3) + { + if (F.VBlankStarted) + { + _cpuClocksSinceVBL++; + } + + _emulator.CPU.TickFromPPU(); + _cpuSyncCounter = 0; + } + else + { + _cpuSyncCounter++; + } + } + } +} diff --git a/Runtime/PPU.Core.cs.meta b/Runtime/PPU.Core.cs.meta new file mode 100644 index 0000000..db54fa8 --- /dev/null +++ b/Runtime/PPU.Core.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6d671ac51126cd43981be89a0309422 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/PPU.Memory.cs b/Runtime/PPU.Memory.cs new file mode 100644 index 0000000..fb14874 --- /dev/null +++ b/Runtime/PPU.Memory.cs @@ -0,0 +1,130 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Aya.UNes +{ + partial class PPU + { + private readonly byte[] _oam = new byte[0x100]; + private readonly byte[] _vRam = new byte[0x2000]; + private readonly byte[] _paletteRAM = new byte[0x20]; + + private static readonly uint[][] _vRamMirrorLookup = + { + new uint[]{0, 0, 1, 1}, // H + new uint[]{0, 1, 0, 1}, // V + new uint[]{0, 1, 2, 3}, // All + new uint[]{0, 0, 0, 0}, // Upper + new uint[]{1, 1, 1, 1}, // Lower + }; + + private int _lastWrittenRegister; + + public void WriteRegister(uint reg, byte val) + { + reg &= 0xF; + _lastWrittenRegister = val & 0xFF; + switch (reg) + { + case 0x0000: + PPUCTRL = val; + return; + case 0x0001: + PPUMASK = val; + return; + case 0x0002: return; + case 0x0003: + OAMADDR = val; + return; + case 0x0004: + OAMDATA = val; + return; + case 0x005: + PPUSCROLL = val; + return; + case 0x0006: + PPUADDR = val; + return; + case 0x0007: + PPUDATA = val; + return; + } + + throw new NotImplementedException($"{reg:X4} = {val:X2}"); + } + + public byte ReadRegister(uint reg) + { + reg &= 0xF; + switch (reg) + { + case 0x0000: return (byte)_lastWrittenRegister; + case 0x0001: return (byte)_lastWrittenRegister; + case 0x0002: + return (byte)PPUSTATUS; + case 0x0003: + return (byte)OAMADDR; + case 0x0004: + return (byte)OAMDATA; + case 0x0005: return (byte)_lastWrittenRegister; + case 0x0006: return (byte)_lastWrittenRegister; + case 0x0007: + return (byte)PPUDATA; + } + throw new NotImplementedException(reg.ToString("X2")); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint GetVRamMirror(long address) + { + long entry; + var table = Math.DivRem(address - 0x2000, 0x400, out entry); + return _vRamMirrorLookup[(int)_emulator.Cartridge.MirroringMode][table] * 0x400 + (uint)entry; + } + + protected override void InitializeMemoryMap() + { + base.InitializeMemoryMap(); + + MapReadHandler(0x2000, 0x2FFF, address => _vRam[GetVRamMirror(address)]); + MapReadHandler(0x3000, 0x3EFF, address => _vRam[GetVRamMirror(address - 0x1000)]); + MapReadHandler(0x3F00, 0x3FFF, address => + { + if (address == 0x3F10 || address == 0x3F14 || address == 0x3F18 || address == 0x3F0C) + { + address -= 0x10; + } + + return _paletteRAM[(address - 0x3F00) & 0x1F]; + }); + + MapWriteHandler(0x2000, 0x2FFF, (address, val) => _vRam[GetVRamMirror(address)] = val); + MapWriteHandler(0x3000, 0x3EFF, (address, val) => _vRam[GetVRamMirror(address - 0x1000)] = val); + MapWriteHandler(0x3F00, 0x3FFF, (address, val) => + { + if (address == 0x3F10 || address == 0x3F14 || address == 0x3F18 || address == 0x3F0C) + { + address -= 0x10; + } + + _paletteRAM[(address - 0x3F00) & 0x1F] = val; + }); + + _emulator.Mapper.InitializeMemoryMap(this); + } + + public void PerformDMA(uint from) + { + //Console.WriteLine("OAM DMA"); + from <<= 8; + for (uint i = 0; i <= 0xFF; i++) + { + _oam[F.OAMAddress] = (byte)_emulator.CPU.ReadByte(from); + from++; + F.OAMAddress++; + } + + _emulator.CPU.Cycle += 513 + _emulator.CPU.Cycle % 2; + } + } +} diff --git a/Runtime/PPU.Memory.cs.meta b/Runtime/PPU.Memory.cs.meta new file mode 100644 index 0000000..cf6d09f --- /dev/null +++ b/Runtime/PPU.Memory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa442177f52c5ff4a805fb4e1a30703b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/PPU.Registers.cs b/Runtime/PPU.Registers.cs new file mode 100644 index 0000000..3c37945 --- /dev/null +++ b/Runtime/PPU.Registers.cs @@ -0,0 +1,261 @@ +using System; + +namespace Aya.UNes +{ + partial class PPU + { + public class PPUFlags + { + /* PPUCTRL register */ + public bool NMIEnabled; + public bool IsMaster; + public bool TallSpritesEnabled; + public uint PatternTableAddress; + public uint SpriteTableAddress; + public uint VRAMIncrement; + + /* PPUMASK register */ + public bool GrayscaleEnabled; + public bool DrawLeftBackground; + public bool DrawLeftSprites; + public bool DrawBackground; + public bool DrawSprites; + // Flipped for PAL/Dendy + public bool EmphasizeRed; + public bool EmphasizeGreen; + public bool EmphasizeBlue; + + /* PPUSTATUS register */ + public bool VBlankStarted; + public bool Sprite0Hit; + public bool SpriteOverflow; + public bool AddressLatch; + + /* PPUADDR register */ + private uint _busAddress; + public uint BusAddress + { + get => _busAddress; + set => _busAddress = value & 0x3FFF; + } + + /* PPUDATA register */ + public uint BusData; + + /* OAMADDR register */ + private uint _oamAddress; + public uint OAMAddress + { + get => _oamAddress; + set => _oamAddress = value & 0xFF; + } + + /* PPUSCROLL registers */ + [Obsolete] + public uint ScrollX; + [Obsolete] + public uint ScrollY; + + public bool RenderingEnabled => DrawBackground || DrawSprites; + } + + public PPUFlags F = new PPUFlags(); + + private uint _v; + public uint V + { + get => _v; + set => _v = value & 0x7FFF; + } + public uint T, X; + + public uint CoarseX => V & 0x1F; + + public uint CoarseY => (V >> 5) & 0x1F; + + public uint FineY => (V >> 12) & 0x7; + + public void ReloadScrollX() => V = (V & 0xFBE0) | (T & 0x041F); + + public void ReloadScrollY() => V = (V & 0x841F) | (T & 0x7BE0); + + public void IncrementScrollX() + { + if ((V & 0x001F) == 31) // if coarse X == 31 + { + V &= ~0x001Fu; // coarse X = 0 + V ^= 0x0400; // switch horizontal nametable + } + else + { + V += 1; // increment coarse X + } + } + + public void IncrementScrollY() + { + if ((V & 0x7000) != 0x7000) // if fine Y < 7 + { + V += 0x1000; // increment fine Y + } + else + { + V &= ~0x7000u; // fine Y = 0 + + uint y = (V & 0x03E0) >> 5; // let y = coarse Y + if (y == 29) + { + y = 0; // coarse Y = 0 + V ^= 0x0800; + } + // switch vertical nametable + else if (y == 31) + { + y = 0; // coarse Y = 0, nametable not switched + } + else + { + y += 1; // increment coarse Y + } + + V = (V & ~0x03E0u) | (y << 5); // put coarse Y back into v + } + } + + public uint PPUCTRL + { + set + { + F.NMIEnabled = (value & 0x80) > 0; + F.IsMaster = (value & 0x40) > 0; + F.TallSpritesEnabled = (value & 0x20) > 0; + F.PatternTableAddress = (value & 0x10) > 0 ? 0x1000u : 0x0000; + F.SpriteTableAddress = (value & 0x08) > 0 ? 0x1000u : 0x0000; + F.VRAMIncrement = (value & 0x04) > 0 ? 32u : 1; + // yyy NN YYYYY XXXXX + // ||| || ||||| +++++--coarse X scroll + // ||| || +++++--------coarse Y scroll + // ||| ++--------------nametable select + // +++-----------------fine Y scroll + T = (T & 0xF3FF) | ((value & 0x3) << 10); // Bits 10-11 hold the base address of the nametable minus $2000 + } + } + + public uint PPUMASK + { + set + { + F.GrayscaleEnabled = (value & 0x1) > 0; + F.DrawLeftBackground = (value & 0x2) > 0; + F.DrawLeftSprites = (value & 0x4) > 0; + F.DrawBackground = (value & 0x8) > 0; + F.DrawSprites = (value & 0x10) > 0; + F.EmphasizeRed = (value & 0x20) > 0; + F.EmphasizeGreen = (value & 0x40) > 0; + F.EmphasizeBlue = (value & 0x80) > 0; + } + } + + /** $2002 **/ + public uint PPUSTATUS + { + get + { + F.AddressLatch = false; + var ret = (F.VBlankStarted.AsByte() << 7) | + (F.Sprite0Hit.AsByte() << 6) | + (F.SpriteOverflow.AsByte() << 5) | + (_lastWrittenRegister & 0x1F); + F.VBlankStarted = false; + return (uint)ret; + } + } + + /** $2006 **/ + public uint PPUADDR + { + set + { + if (F.AddressLatch) + { + T = (T & 0xFF00) | value; + F.BusAddress = T; + V = T; + } + else + { + T = (T & 0x80FF) | ((value & 0x3F) << 8); + } + + F.AddressLatch ^= true; + } + } + + /** $2005 **/ + public uint PPUSCROLL + { + set + { + if (F.AddressLatch) + { + F.ScrollY = value; + T = (T & 0x8FFF) | ((value & 0x7) << 12); + T = (T & 0xFC1F) | (value & 0xF8) << 2; + } + else + { + F.ScrollX = value; + X = value & 0x7; + T = (T & 0xFFE0) | (value >> 3); + } + + F.AddressLatch ^= true; + } + } + + private uint _readBuffer; + public uint PPUDATA + { + get + { + uint ret = ReadByte(F.BusAddress); + if (F.BusAddress < 0x3F00) + { + uint temp = _readBuffer; + _readBuffer = ret; + ret = temp; + } + else + { + // Palette read should also read VRAM into read buffer + _readBuffer = ReadByte(F.BusAddress - 0x1000); + } + + F.BusAddress += F.VRAMIncrement; + return ret; + } + set + { + F.BusData = value; + WriteByte(F.BusAddress, value); + F.BusAddress += F.VRAMIncrement; + } + } + + public uint OAMADDR + { + get => F.OAMAddress; + set => F.OAMAddress = value; + } + + public uint OAMDATA + { + get => _oam[F.OAMAddress]; + set + { + _oam[F.OAMAddress] = (byte)value; + F.OAMAddress++; + } + } + } +} diff --git a/Runtime/PPU.Registers.cs.meta b/Runtime/PPU.Registers.cs.meta new file mode 100644 index 0000000..24948ac --- /dev/null +++ b/Runtime/PPU.Registers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0db554be3f472c940bde6ce34fc9ed7e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/PPU.cs b/Runtime/PPU.cs new file mode 100644 index 0000000..c3a08c7 --- /dev/null +++ b/Runtime/PPU.cs @@ -0,0 +1,10 @@ +namespace Aya.UNes +{ + public sealed partial class PPU : Addressable + { + public PPU(Emulator emulator) : base(emulator, 0x3FFF) + { + InitializeMemoryMap(); + } + } +} diff --git a/Runtime/PPU.cs.meta b/Runtime/PPU.cs.meta new file mode 100644 index 0000000..5006815 --- /dev/null +++ b/Runtime/PPU.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd14757720b242e4b92425c9d6aae16a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Renderer.meta b/Runtime/Renderer.meta new file mode 100644 index 0000000..ede6704 --- /dev/null +++ b/Runtime/Renderer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff3612d2884e82145a83b869175fe872 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Renderer/IRenderer.cs b/Runtime/Renderer/IRenderer.cs new file mode 100644 index 0000000..683b552 --- /dev/null +++ b/Runtime/Renderer/IRenderer.cs @@ -0,0 +1,13 @@ +namespace Aya.UNes.Renderer +{ + public interface IRenderer + { + string RendererName { get; } + + void Draw(); + + void InitRendering(UNes nes); + + void EndRendering(); + } +} diff --git a/Runtime/Renderer/IRenderer.cs.meta b/Runtime/Renderer/IRenderer.cs.meta new file mode 100644 index 0000000..5bad6e1 --- /dev/null +++ b/Runtime/Renderer/IRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e02ede35ded0294fb7f13f60387557f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Renderer/UnityRenderer.cs b/Runtime/Renderer/UnityRenderer.cs new file mode 100644 index 0000000..3881f32 --- /dev/null +++ b/Runtime/Renderer/UnityRenderer.cs @@ -0,0 +1,65 @@ +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Aya.UNes.Renderer +{ + public class UnityRenderer : IRenderer + { + public UNes UNes; + public RenderTexture RenderTexture; + + public string RendererName => "Unity"; + + private Texture2D _drawTexture; + private Color[] _pixelCache; + + public void Draw() + { + if (RenderTexture.filterMode != UNes.FilterMode) + { + RenderTexture.filterMode = UNes.FilterMode; + } + + for (var y = 0; y < UNes.GameHeight; y++) + { + for (var x = 0; x < UNes.GameWidth; x++) + { + var rawIndex = UNes.GameWidth * y + x; + var color = GetColor(UNes.RawBitmap[rawIndex]); + var texIndex = UNes.GameWidth * (UNes.GameHeight - y - 1) + x; + _pixelCache[texIndex] = color; + } + } + + _drawTexture.SetPixels(_pixelCache); + _drawTexture.Apply(); + + Graphics.Blit(_drawTexture, RenderTexture); + } + + public Color GetColor(uint value) + { + var r = 0xFF0000 & value; + r >>= 16; + var b = 0xFF & value; + var g = 0xFF00 & value; + g >>= 8; + var color = new Color(r / 255f, g / 255f, b / 255f); + return color; + } + + public void InitRendering(UNes nes) + { + UNes = nes; + RenderTexture = nes.RenderTexture; + _drawTexture = new Texture2D(UNes.GameWidth, UNes.GameHeight); + _pixelCache = new Color[UNes.GameWidth * UNes.GameHeight]; + } + + public void EndRendering() + { + + } + } +} + diff --git a/Runtime/Renderer/UnityRenderer.cs.meta b/Runtime/Renderer/UnityRenderer.cs.meta new file mode 100644 index 0000000..13e31a8 --- /dev/null +++ b/Runtime/Renderer/UnityRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f751adeb00750a43a0d2534a2301da2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UNes.cs b/Runtime/UNes.cs new file mode 100644 index 0000000..f7d6c47 --- /dev/null +++ b/Runtime/UNes.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using UnityEngine; +using Aya.UNes.Controller; +using Aya.UNes.Renderer; +using Debug = UnityEngine.Debug; + +namespace Aya.UNes +{ + public class UNes : MonoBehaviour + { + public string RomFile; + public RenderTexture RenderTexture; + + public const int GameWidth = 256; + public const int GameHeight = 240; + public FilterMode FilterMode = FilterMode.Point; + public bool RenderThread = true; + + + private bool _rendererRunning = true; + private readonly IController _controller = new NesController(); + + private Emulator _emu; + private bool _suspended; + + private readonly Type[] _possibleRendererList = { typeof(UnityRenderer) }; + private readonly List _availableRendererList = new List(); + + public IRenderer Renderer { get; set; } + public uint[] RawBitmap { get; set; } = new uint[GameWidth * GameHeight]; + public bool Ready { get; set; } + public bool GameStarted { get; set; } + + private Thread _renderThread; + private int _activeSpeed = 1; + + private void BootCartridge(string rom) + { + var bytes = Resources.Load(RomFile).bytes; + _emu = new Emulator(bytes, _controller); + + if (RenderThread) + { + _renderThread = new Thread(() => + { + GameStarted = true; + Console.WriteLine(_emu.Cartridge); + var s = new Stopwatch(); + var s0 = new Stopwatch(); + while (_rendererRunning) + { + if (_suspended) + { + Thread.Sleep(100); + continue; + } + + s.Restart(); + for (var i = 0; i < 60 && !_suspended; i++) + { + s0.Restart(); + lock (RawBitmap) + { + _emu.PPU.ProcessFrame(); + RawBitmap = _emu.PPU.RawBitmap; + } + + s0.Stop(); + Thread.Sleep(Math.Max((int)(980 / 60.0 - s0.ElapsedMilliseconds), 0) / _activeSpeed); + } + + s.Stop(); + Console.WriteLine($"60 frames in {s.ElapsedMilliseconds}ms"); + } + }); + + _renderThread.Start(); + } + else + { + GameStarted = true; + } + } + + #region Monobehaviour + + public void Awake() + { + FindRenderer(); + SetRenderer(_availableRendererList.Last()); + } + + public void OnEnable() + { + BootCartridge(RomFile); + } + + public void OnDisable() + { + _rendererRunning = false; + // _emu?.Save(); + } + + public void Update() + { + if (!GameStarted) return; + UpdateInput(); + UpdateRender(); + } + + #endregion + + #region Input + + public void UpdateInput() + { + HandlerKeyDown(keyCode => + { + switch (keyCode) + { + case KeyCode.F2: + _suspended = false; + break; + case KeyCode.F3: + _suspended = true; + break; + default: + _controller.PressKey(keyCode); + break; + } + }); + + HandlerKeyUp(keyCode => + { + _controller.ReleaseKey(keyCode); + }); + } + + private KeyCode[] _keyCodes + { + get + { + if (_keyCodeCache == null) + { + var array = Enum.GetValues(typeof(KeyCode)); + _keyCodeCache = new KeyCode[array.Length]; + for (var i = 0; i < array.Length; i++) + { + _keyCodeCache[i] = (KeyCode) array.GetValue(i); + } + } + + return _keyCodeCache; + } + } + private KeyCode[] _keyCodeCache; + + public void HandlerKeyDown(Action onKeyDown) + { + foreach (var keyCode in _keyCodes) + { + if (Input.GetKeyDown(keyCode)) + { + onKeyDown(keyCode); + } + } + } + + public void HandlerKeyUp(Action onKeyUp) + { + foreach (var keyCode in _keyCodes) + { + if (Input.GetKeyUp(keyCode)) + { + onKeyUp(keyCode); + } + } + } + + #endregion + + #region Render + + public void UpdateRender() + { + if (!RenderThread) + { + if (!_rendererRunning) return; + if (_suspended) return; + + _emu.PPU.ProcessFrame(); + RawBitmap = _emu.PPU.RawBitmap; + Renderer.Draw(); + } + else + { + if (!_rendererRunning) return; + if (_suspended) return; + + lock (RawBitmap) + { + Renderer.Draw(); + } + } + } + + private void SetRenderer(IRenderer render) + { + if (Renderer == render) return; + Renderer?.EndRendering(); + Renderer = render; + render.InitRendering(this); + } + + private void FindRenderer() + { + foreach (var renderType in _possibleRendererList) + { + try + { + var renderer = (IRenderer)Activator.CreateInstance(renderType); + renderer.InitRendering(this); + renderer.EndRendering(); + _availableRendererList.Add(renderer); + } + catch (Exception) + { + Console.WriteLine($"{renderType} failed to initialize"); + } + } + } + + #endregion + + public void Open(string file) + { + try + { + BootCartridge(file); + } + catch (Exception ex) + { + Debug.LogError("Error loading ROM file; either corrupt or unsupported"); + } + } + } +} diff --git a/Runtime/UNes.cs.meta b/Runtime/UNes.cs.meta new file mode 100644 index 0000000..99998ce --- /dev/null +++ b/Runtime/UNes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5948479853ec1194497c60babb6bc213 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utility.cs b/Runtime/Utility.cs new file mode 100644 index 0000000..3085f25 --- /dev/null +++ b/Runtime/Utility.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Aya.UNes +{ + public static class Utility + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte AsByte(this bool to) + { + unsafe + { + var result = *((byte*) &to); + return result; + } + } + + public static void Fill(this T[] arr, T value) + { + for (var i = 0; i < arr.Length; i++) + { + arr[i] = value; + } + } + + public static void Map(this IEnumerable enumerator, Action go) + { + foreach (var e in enumerator) + { + go(e); + } + } + } +} diff --git a/Runtime/Utility.cs.meta b/Runtime/Utility.cs.meta new file mode 100644 index 0000000..2d1f3ff --- /dev/null +++ b/Runtime/Utility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b326828d1b42abc419d9fe125eed8205 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample.meta b/Sample.meta new file mode 100644 index 0000000..46826c5 --- /dev/null +++ b/Sample.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 049e5a5b3f9646b4fab19ebeb26449a1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample/Resources.meta b/Sample/Resources.meta new file mode 100644 index 0000000..dfacc8d --- /dev/null +++ b/Sample/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 481be8b9dd1218749a75bf1bd4612c04 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample/Resources/Mario.bytes b/Sample/Resources/Mario.bytes new file mode 100644 index 0000000..878ef21 Binary files /dev/null and b/Sample/Resources/Mario.bytes differ diff --git a/Sample/Resources/Mario.bytes.meta b/Sample/Resources/Mario.bytes.meta new file mode 100644 index 0000000..78d1e2d --- /dev/null +++ b/Sample/Resources/Mario.bytes.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 040fea71e1812ce45bd2b72c8ad2e2ae +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample/Scene.meta b/Sample/Scene.meta new file mode 100644 index 0000000..a2af5ba --- /dev/null +++ b/Sample/Scene.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 97287d4650544eb4daa041e4a384a06b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample/Scene/UNes_Demo.unity b/Sample/Scene/UNes_Demo.unity new file mode 100644 index 0000000..6747474 --- /dev/null +++ b/Sample/Scene/UNes_Demo.unity @@ -0,0 +1,825 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_UseShadowmask: 1 +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &537454904 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 537454906} + - component: {fileID: 537454905} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &537454905 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 537454904} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &537454906 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 537454904} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &589359925 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 589359928} + - component: {fileID: 589359927} + - component: {fileID: 589359926} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &589359926 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 589359925} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &589359927 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 589359925} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &589359928 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 589359925} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &622947734 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 622947735} + - component: {fileID: 622947737} + - component: {fileID: 622947736} + m_Layer: 5 + m_Name: Game + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &622947735 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 622947734} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1361633018} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &622947736 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 622947734} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Texture: {fileID: 8400000, guid: 6645567e4c11d9447b1aee2406f681c5, type: 2} + m_UVRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 +--- !u!222 &622947737 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 622947734} + m_CullTransparentMesh: 0 +--- !u!1 &1355724343 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1355724346} + - component: {fileID: 1355724345} + - component: {fileID: 1355724344} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1355724344 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1355724343} + m_Enabled: 1 +--- !u!20 &1355724345 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1355724343} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1355724346 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1355724343} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1361633014 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1361633018} + - component: {fileID: 1361633017} + - component: {fileID: 1361633016} + - component: {fileID: 1361633015} + m_Layer: 5 + m_Name: UI + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1361633015 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1361633014} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &1361633016 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1361633014} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!223 &1361633017 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1361633014} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &1361633018 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1361633014} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 622947735} + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &1434955706 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1434955708} + - component: {fileID: 1434955707} + m_Layer: 0 + m_Name: UNes + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1434955707 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1434955706} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5948479853ec1194497c60babb6bc213, type: 3} + m_Name: + m_EditorClassIdentifier: + RomFile: Mario + RenderTexture: {fileID: 8400000, guid: 6645567e4c11d9447b1aee2406f681c5, type: 2} + FilterMode: 0 + RenderThread: 1 +--- !u!4 &1434955708 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1434955706} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1760781627 +GameObject: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1760781629} + - component: {fileID: 1760781628} + m_Layer: 0 + m_Name: UndoProRecords + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1760781628 +MonoBehaviour: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1760781627} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: df9ab10aeab793d47a27405557d0b929, type: 3} + m_Name: + m_EditorClassIdentifier: + undoProRecords: [] + undoState: + redoRecords: [] + undoRecords: + - Selection Change + - Selection Change + - Inspector + - Inspector + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Inspector + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Selection Change + - Modify Rainbow Folder Ruleset +--- !u!4 &1760781629 +Transform: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1760781627} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Sample/Scene/UNes_Demo.unity.meta b/Sample/Scene/UNes_Demo.unity.meta new file mode 100644 index 0000000..fa040cd --- /dev/null +++ b/Sample/Scene/UNes_Demo.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: eb0c18a619175384d95147898a43054b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample/Texture.meta b/Sample/Texture.meta new file mode 100644 index 0000000..deb6af3 --- /dev/null +++ b/Sample/Texture.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b52033f7628c4346a6dd0b2806e75ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sample/Texture/RenderTexture.renderTexture b/Sample/Texture/RenderTexture.renderTexture new file mode 100644 index 0000000..134040c --- /dev/null +++ b/Sample/Texture/RenderTexture.renderTexture @@ -0,0 +1,37 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!84 &8400000 +RenderTexture: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RenderTexture + m_ImageContentsHash: + serializedVersion: 2 + Hash: 00000000000000000000000000000000 + m_ForcedFallbackFormat: 4 + m_DownscaleFallback: 0 + serializedVersion: 3 + m_Width: 256 + m_Height: 240 + m_AntiAliasing: 1 + m_MipCount: -1 + m_DepthFormat: 2 + m_ColorFormat: 8 + m_MipMap: 0 + m_GenerateMips: 1 + m_SRGB: 0 + m_UseDynamicScale: 0 + m_BindMS: 0 + m_EnableCompatibleFormat: 1 + m_TextureSettings: + serializedVersion: 2 + m_FilterMode: 0 + m_Aniso: 0 + m_MipBias: 0 + m_WrapU: 1 + m_WrapV: 1 + m_WrapW: 1 + m_Dimension: 2 + m_VolumeDepth: 1 diff --git a/Sample/Texture/RenderTexture.renderTexture.meta b/Sample/Texture/RenderTexture.renderTexture.meta new file mode 100644 index 0000000..d8035c9 --- /dev/null +++ b/Sample/Texture/RenderTexture.renderTexture.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6645567e4c11d9447b1aee2406f681c5 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: