diff --git a/Sources/SwiftNES/AddressableDevice.swift b/Sources/SwiftNES/AddressableDevice.swift index 2b0c979..aa618ca 100644 --- a/Sources/SwiftNES/AddressableDevice.swift +++ b/Sources/SwiftNES/AddressableDevice.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// A protocol that describes a device that is addressable on a system bus. protocol AddressableDevice: AnyObject { /// The address range of the device. diff --git a/Sources/SwiftNES/AudioReceiver.swift b/Sources/SwiftNES/AudioReceiver.swift index 0998249..b1def2a 100644 --- a/Sources/SwiftNES/AudioReceiver.swift +++ b/Sources/SwiftNES/AudioReceiver.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - public protocol AudioReceiver: AnyObject { // TODO: Define an AudioReceiver. // TODO: Move protocol into an SDK package. diff --git a/Sources/SwiftNES/Bus.swift b/Sources/SwiftNES/Bus.swift index ca35a07..4f1e5da 100644 --- a/Sources/SwiftNES/Bus.swift +++ b/Sources/SwiftNES/Bus.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// Represents a bus that allows the CPU to read and write to various attached devices via memory address. final class Bus { @@ -30,7 +28,7 @@ final class Bus { /// Initializes a bus with attached devices. /// /// - Parameters: - /// - addressableDevices: The devices that can be addressed via this bus. + /// - addressableDevices: The ``AddressableDevice``s that can be addressed via the bus. init(addressableDevices: [any AddressableDevice]) throws { // TODO: Verify that there are no overlapping devices. @@ -49,8 +47,8 @@ final class Bus { /// Read from or write to an address on the bus. /// /// - Parameters: - /// - address: The address to read from or write to.. - /// - Returns: The value that was read from a device on the bus. + /// - address: The ``Address`` to read from or write to. + /// - Returns: The ``Value`` that was read from a device on the bus. subscript(address: Address) -> Value { get { read(from: address) } set { write(newValue, to: address) } @@ -61,8 +59,8 @@ final class Bus { /// - Note: If a device does not respond to the address, `0` will be returned. /// /// - Parameters: - /// - address: The address to read from. - /// - Returns: The value that was read from a device on the bus. + /// - address: The ``Address`` to read from. + /// - Returns: The ``Value`` that was read from a device on the bus. func read(from address: Address) -> Value { guard let deviceToReadFromIndex = addressableReadDeviceRanges .firstIndex(where: { $0.contains(address) }) else { return 0 } @@ -77,8 +75,8 @@ final class Bus { /// - Note: If a device does not respond to the address, this method does nothing. /// /// - Parameters: - /// - value: The value to write to the bus. - /// - address: The address to write to. + /// - value: The ``Value`` to write to the bus. + /// - address: The ``Address`` to write to. func write(_ value: Value, to address: Address) { guard let deviceToWriteToIndex = addressableWriteDeviceRanges .firstIndex(where: { $0.contains(address) }) else { return } @@ -91,15 +89,21 @@ final class Bus { // MARK: - Private - /// The ranges of the addressable devices attached to the bus that can be read from. + /// The ranges of the ``AddressableReadDevice``s attached to the bus. private let addressableReadDeviceRanges: [AddressRange] - /// The ranges of the addressable devices attached to the bus that can be written to. + /// The ranges of the ``AddressableWriteDevice``s attached to the bus. private let addressableWriteDeviceRanges: [AddressRange] - /// All of the addressable devices attached to the bus that can be read from. + /// All of the ``AddressableReadDevice``s attached to the bus. private let addressableReadDevices: [any AddressableReadDevice] - /// All of the addressable devices attached to the bus that can be written to. + /// All of the ``AddressableWriteDevice``s attached to the bus. private let addressableWriteDevices: [any AddressableWriteDevice] } + +extension Bus: DirectMemoryAccessableReadDevice { + func dmaRead(from address: Address) -> Value { + read(from: address) + } +} diff --git a/Sources/SwiftNES/CartridgeConnector.swift b/Sources/SwiftNES/CartridgeConnector.swift index 92fe2c8..934977a 100644 --- a/Sources/SwiftNES/CartridgeConnector.swift +++ b/Sources/SwiftNES/CartridgeConnector.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// A class that represents a connection to a cartridge. /// /// Instead of attaching the cartridge directly to the bus, a connector is used to allow for diff --git a/Sources/SwiftNES/ControllerConnector.swift b/Sources/SwiftNES/ControllerConnector.swift index 6d33265..92db273 100644 --- a/Sources/SwiftNES/ControllerConnector.swift +++ b/Sources/SwiftNES/ControllerConnector.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// A controller that can be connected to a ``ControllerConnector``. public protocol Controller: AnyObject { /// Reads a single bit from the controller. diff --git a/Sources/SwiftNES/Controllers/ControlPad.swift b/Sources/SwiftNES/Controllers/ControlPad.swift index 95a5af2..5b4e901 100644 --- a/Sources/SwiftNES/Controllers/ControlPad.swift +++ b/Sources/SwiftNES/Controllers/ControlPad.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// A class that represents an NES Control Pad (NES-004). public final class ControlPad: Controller { diff --git a/Sources/SwiftNES/DirectMemoryAccessController.swift b/Sources/SwiftNES/DirectMemoryAccessController.swift index 629e4f4..1159edc 100644 --- a/Sources/SwiftNES/DirectMemoryAccessController.swift +++ b/Sources/SwiftNES/DirectMemoryAccessController.swift @@ -1,6 +1,6 @@ // MIT License // -// Copyright (c) 2020 Jerrod Putman +// Copyright (c) 2023 Jerrod Putman // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,26 +20,119 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation +/// Errors that are thrown by the ``DirectMemoryAccessController``. +enum DirectMemoryAccessControllerError: Error { + /// The ``DirectMemoryAccessController/readDevice`` was not assigned. + case readDeviceNotAssigned + + /// The ``DirectMemoryAccessController/writeDevice`` was not assigned. + case writeDeviceNotAssigned +} -struct ObjectAttributeEntry { - var y: UInt8 - var id: UInt8 - var attribute: UInt8 - var x: UInt8 +/// Represents a device that the ``DirectMemoryAccessController`` reads from. +protocol DirectMemoryAccessableReadDevice: AnyObject { + /// Reads a value from the device at the specified address. + /// + /// - Parameter address: The ``Address`` of the device to read from. + /// - Returns: The ``Value`` stored at the address on the device. + func dmaRead(from address: Address) -> Value } -var objectAttributeMemory: [ObjectAttributeEntry] = [] +/// Represents a device that the ``DirectMemoryAccessController`` writes to. +protocol DirectMemoryAccessableWriteDevice: AnyObject { + /// Writes a value to the device at the specified address. + /// + /// - Parameter value: The ``Value`` to write to the device. + /// - Parameter address: The 8-bit address to write to on the device. + func dmaWrite(_ value: Value, to address: UInt8) +} -final class DirectMemoryAccessController { +/// Represents the direct memory access controller of the NES. +final class DirectMemoryAccessController: AddressableWriteDevice { - init() { - page = 0 - address = 0 - data = 0 + // MARK: - Initializers + + /// Initializes the controller. + /// + /// - Parameter address: The ``Address`` where the controller can be written to. + init(address: Address) { + self.addressRange = address...address + + self.page = 0 + self.address = 0 + self.data = 0 } + + // MARK: - Getting the status of the controller + + /// Whether or not a DMA transfer is in progress. + private(set) var isTransferInProgress = false + + + // MARK: - Transferring data + + /// The device to read from during transfers. + weak var readDevice: (any DirectMemoryAccessableReadDevice)? + + /// The device to write to during transfers. + weak var writeDevice: (any DirectMemoryAccessableWriteDevice)? + + /// Clocks the controller. + /// + /// - Parameter cycleCount: The current cycle count of the hardware. + /// - Returns: Whether or not a transfer is in progress. + /// - Throws: A ``DirectMemoryAccessControllerError``. + func clock(cycleCount: UInt) throws -> Bool { + guard isTransferInProgress else { return false } + + guard let readDevice else { throw DirectMemoryAccessControllerError.readDeviceNotAssigned } + guard let writeDevice else { throw DirectMemoryAccessControllerError.writeDeviceNotAssigned } + + if syncCycle { + if cycleCount % 2 == 1 { + syncCycle = false + } + } else { + if cycleCount % 2 == 0 { + data = readDevice.dmaRead(from: UInt16(lo: address, hi: page)) + } else { + writeDevice.dmaWrite(data, to: address) + address &+= 1 + + if address == 0 { + isTransferInProgress = false + syncCycle = true + } + } + } + + return true + } + + + // MARK: - AddressableWriteDevice + + let addressRange: AddressRange + + func write(_ data: Value, to address: Address) { + page = data + self.address = 0 + isTransferInProgress = true + } + + + // MARK: - Private + + /// The page of memory where the current DMA transfer is occurring. private var page: UInt8 + + /// The address of memory, offset from the page, where the current DMA transfer is occurring. private var address: UInt8 + + /// The data that is currently being moved from the CPU to the PPU. private var data: Value + + /// Whether or not the transfer should wait for synchronization. + private var syncCycle = true } diff --git a/Sources/SwiftNES/MOS6502.swift b/Sources/SwiftNES/MOS6502.swift index 7beba27..08cef30 100644 --- a/Sources/SwiftNES/MOS6502.swift +++ b/Sources/SwiftNES/MOS6502.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// A class that emulates the behavior of the MOS Technology 6502 microprocessor. final class MOS6502 { @@ -112,7 +110,7 @@ final class MOS6502 { status.insert(.unused) // Increment the program counter since we read the opcode byte. - pc += 1 + pc &+= 1 // Get the instruction from the opcode. let instruction = MOS6502.instructions[Int(opcode)] @@ -166,16 +164,16 @@ final class MOS6502 { // Push the program counter to the stack. // The program counter is 16-bits, so we require 2 writes. bus[0x0100 + Address(stkp)] = pc.hi - stkp -= 1 + stkp &-= 1 bus[0x0100 + Address(stkp)] = pc.lo - stkp -= 1 + stkp &-= 1 // Push the status register to the stack. status.remove(.break) status.insert(.unused) status.insert(.disableInterrupts) bus[0x0100 + Address(stkp)] = status.rawValue - stkp -= 1 + stkp &-= 1 // Read the new program counter location from a specific address. let irqAddress: Address = 0xfffe @@ -192,16 +190,16 @@ final class MOS6502 { // Push the program counter to the stack. // The program counter is 16-bits, so we require 2 writes. bus[0x0100 + Address(stkp)] = pc.hi - stkp -= 1 + stkp &-= 1 bus[0x0100 + Address(stkp)] = pc.lo - stkp -= 1 + stkp &-= 1 // Push the status register to the stack. status.remove(.break) status.insert(.unused) status.insert(.disableInterrupts) bus[0x0100 + Address(stkp)] = status.rawValue - stkp -= 1 + stkp &-= 1 // Read the new program counter location from a specific address. let nmiAddress: Address = 0xfffa @@ -853,7 +851,7 @@ extension MOS6502 { } private func BRK() { - pc += 1 + pc &+= 1 status.insert(.disableInterrupts) bus[0x0100 + Address(stkp)] = pc.hi @@ -970,7 +968,7 @@ extension MOS6502 { } private func JSR(_ address: Address) { - pc -= 1 + pc &-= 1 bus[0x0100 + Address(stkp)] = pc.hi stkp &-= 1 @@ -1116,7 +1114,7 @@ extension MOS6502 { stkp &+= 1 pc |= Address(bus[0x0100 + Address(stkp)]) << 8 - pc += 1 + pc &+= 1 } private func SEC() { @@ -1203,7 +1201,7 @@ extension MOS6502 { /// The next byte is used as a value. private func IMM() -> ReadAddressResult { let address = pc - pc += 1 + pc &+= 1 return (address, false) } @@ -1214,7 +1212,7 @@ extension MOS6502 { private func ABS() -> ReadAddressResult { let address = Address(lo: bus[pc + 0], hi: bus[pc + 1]) - pc += 2 + pc &+= 2 return (address, false) } @@ -1225,10 +1223,10 @@ extension MOS6502 { private func ABX() -> ReadAddressResult { let lo = bus[pc + 0] let hi = bus[pc + 1] - pc += 2 + pc &+= 2 var address = Address(lo: lo, hi: hi) - address += Address(x) + address &+= Address(x) let crossedPageBoundary = (address & 0xff00) != (hi << 8) @@ -1241,7 +1239,7 @@ extension MOS6502 { private func ABY() -> ReadAddressResult { let lo = bus[pc + 0] let hi = bus[pc + 1] - pc += 2 + pc &+= 2 var address = Address(lo: lo, hi: hi) address &+= Address(y) @@ -1256,7 +1254,7 @@ extension MOS6502 { /// The read address is used as a relative offset to the current address of the program counter. private func REL() -> ReadAddressResult { var relativeAddress = Address(bus[pc]) - pc += 1 + pc &+= 1 if (relativeAddress & 0x80) > 0 { relativeAddress |= 0xFF00; @@ -1272,7 +1270,7 @@ extension MOS6502 { private func ZP0() -> ReadAddressResult { var address = Address(bus[pc]) address &= 0x00ff - pc += 1 + pc &+= 1 return (address, false) } @@ -1284,7 +1282,7 @@ extension MOS6502 { private func ZPX() -> ReadAddressResult { var address = Address(bus[pc] &+ x) address &= 0x00ff - pc += 1 + pc &+= 1 return (address, false) } @@ -1296,7 +1294,7 @@ extension MOS6502 { private func ZPY() -> ReadAddressResult { var address = Address(bus[pc] &+ y) address &= 0x00ff - pc += 1 + pc &+= 1 return (address, false) } @@ -1307,7 +1305,7 @@ extension MOS6502 { private func IND() -> ReadAddressResult { let ptrLo = bus[pc + 0] let ptrHi = bus[pc + 1] - pc += 2 + pc &+= 2 let ptr = Address(lo: ptrLo, hi: ptrHi) @@ -1326,7 +1324,7 @@ extension MOS6502 { /// The 8-bit address is offset by the value of the `x` register to index a location in the first page. private func IZX() -> ReadAddressResult { let ptr = Address(bus[pc]) - pc += 1 + pc &+= 1 let lo = bus[(ptr + Address(x)) & 0x00ff] let hi = bus[(ptr + Address(x) + 1) & 0x00ff] @@ -1341,7 +1339,7 @@ extension MOS6502 { /// offset this address. private func IZY() -> ReadAddressResult { let ptr = Address(bus[pc]) - pc += 1 + pc &+= 1 let lo = bus[ptr & 0x00ff] let hi = bus[(ptr + 1) & 0x00ff] diff --git a/Sources/SwiftNES/Mapper.swift b/Sources/SwiftNES/Mapper.swift index 68e6239..02eff23 100644 --- a/Sources/SwiftNES/Mapper.swift +++ b/Sources/SwiftNES/Mapper.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// An enumeration that defines the mapped addresses that can be returned by a mapper. enum MappedAddress { /// The address could not be mapped. diff --git a/Sources/SwiftNES/NES.swift b/Sources/SwiftNES/NES.swift index 4a477f9..64af5ba 100644 --- a/Sources/SwiftNES/NES.swift +++ b/Sources/SwiftNES/NES.swift @@ -36,11 +36,15 @@ public final class NES { ppu = PixelProcessingUnit(bus: ppuBus) ram = try! RandomAccessMemoryDevice(memorySize: 0x0800, addressRange: 0x0000...0x1fff) + directMemoryAccessController = DirectMemoryAccessController(address: 0x4014) controllerConnector1 = ControllerConnector(address: 0x4016) controllerConnector2 = ControllerConnector(address: 0x4017) cpuCartridgeConnector = CartridgeConnector(addressRange: 0x8000...0xffff) - let cpuBus = try Bus(addressableDevices: [ram, ppu, controllerConnector1, controllerConnector2, cpuCartridgeConnector]) + let cpuBus = try Bus(addressableDevices: [ram, ppu, directMemoryAccessController, controllerConnector1, controllerConnector2, cpuCartridgeConnector]) cpu = RP2A03G(bus: cpuBus) + + directMemoryAccessController.readDevice = cpuBus + directMemoryAccessController.writeDevice = ppu } @@ -67,6 +71,9 @@ public final class NES { /// The palette memory. let palette: RandomAccessMemoryDevice + /// The direct memory access (DMA) controller. + let directMemoryAccessController: DirectMemoryAccessController + /// The controller connector for controller port 1. let controllerConnector1: ControllerConnector @@ -85,12 +92,14 @@ public final class NES { } } + /// The controller connected to controller port 1. public var controller1: (any Controller)? = nil { didSet { controllerConnector1.controller = controller1 } } + /// The controller connected to controller port 2. public var controller2: (any Controller)? = nil { didSet { controllerConnector2.controller = controller2 @@ -100,11 +109,13 @@ public final class NES { // MARK: - Connecting to the outputs of the hardware + /// The device that receives the video output. public weak var videoReceiver: (any VideoReceiver)? { get { ppu.videoReceiver } set { ppu.videoReceiver = newValue } } + /// The device that receives the audio output. public weak var audioReceiver: (any AudioReceiver)? @@ -165,7 +176,12 @@ public final class NES { ppu.clock() if clockCount % 3 == 0 { - cpu.core.clock() + // Handle DMA transfers. + // TODO: Properly throw this. + if try! !directMemoryAccessController.clock(cycleCount: clockCount) { + // If no DMA transfer is in progress, then clock the CPU instead. + cpu.core.clock() + } } if ppu.nmi { diff --git a/Sources/SwiftNES/ObjectAttributeEntry.swift b/Sources/SwiftNES/ObjectAttributeEntry.swift new file mode 100644 index 0000000..4245212 --- /dev/null +++ b/Sources/SwiftNES/ObjectAttributeEntry.swift @@ -0,0 +1,36 @@ +// MIT License +// +// Copyright (c) 2023 Jerrod Putman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// Represents the attributes of a single hardware sprite object. +struct ObjectAttributeEntry { + /// The y-position of the sprite. + var y: UInt8 + + /// The tile index of the sprite. + var tileId: UInt8 + + /// Miscellaneous attributes of the sprite. + var attribute: UInt8 + + /// The x-position of the sprite. + var x: UInt8 +} diff --git a/Sources/SwiftNES/PixelProcessingUnit.swift b/Sources/SwiftNES/PixelProcessingUnit.swift index 74fd527..52631a7 100644 --- a/Sources/SwiftNES/PixelProcessingUnit.swift +++ b/Sources/SwiftNES/PixelProcessingUnit.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// A class that emulates the behavior of the 2C02 pixel processing unit. final class PixelProcessingUnit: AddressableReadWriteDevice { @@ -32,6 +30,11 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { /// - parameter bus: The bus that the PPU should use to communicate with other devices. init(bus: Bus) { self.bus = bus + self.objectAttributeMemory = Array(repeating: ObjectAttributeEntry(y: 0, tileId: 0, attribute: 0, x: 0), count: 64) + + scanlineSprites.reserveCapacity(8) + spriteShifterPatternLo.reserveCapacity(8) + spriteShifterPatternHi.reserveCapacity(8) } @@ -46,7 +49,7 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { static let yIncrementMode = Control(rawValue: (1 << 2)) static let patternSprite = Control(rawValue: (1 << 3)) static let patternBackground = Control(rawValue: (1 << 4)) - static let spriteSize = Control(rawValue: (1 << 5)) + static let doubleSpriteHeight = Control(rawValue: (1 << 5)) static let slaveMode = Control(rawValue: (1 << 6)) static let enableNmi = Control(rawValue: (1 << 7)) } @@ -84,6 +87,13 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { /// The mask register. private(set) var mask: Mask = Mask(rawValue: 0) + /// The OAM address register. + private(set) var oamAddress: UInt8 = 0 + + + /// The object attribute memory (OAM). + private(set) var objectAttributeMemory: [ObjectAttributeEntry] + var mirroringMode: Cartridge.MirroringMode = .horizontal @@ -118,7 +128,7 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { if vramAddress.coarseX == 31 { vramAddress.coarseX = 0 - vramAddress.nametableX = ~vramAddress.nametableX + vramAddress.nametableX = vramAddress.nametableX == 0 ? 1 : 0 } else { vramAddress.coarseX += 1 } @@ -134,7 +144,7 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { vramAddress.fineY = 0 if vramAddress.coarseY == 29 { vramAddress.coarseY = 0 - vramAddress.nametableY = ~vramAddress.nametableY + vramAddress.nametableY = vramAddress.nametableY == 0 ? 1 : 0 } else if vramAddress.coarseY == 31 { vramAddress.coarseY = 0 } else { @@ -166,18 +176,35 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { } func updateShifters() { - guard mask.contains(.renderBackground) else { return } + if mask.contains(.renderBackground) { + bgShiftPatternLo <<= 1 + bgShiftPatternHi <<= 1 + bgShiftAttributeLo <<= 1 + bgShiftAttributeHi <<= 1 + } - bgShiftPatternLo <<= 1 - bgShiftPatternHi <<= 1 - bgShiftAttributeLo <<= 1 - bgShiftAttributeHi <<= 1 + if mask.contains(.renderSprites) && cycle >= 1 && cycle < 258 { + for scanlineSpriteIndex in 0.. 0 { + scanlineSprites[scanlineSpriteIndex].x -= 1 + } else { + spriteShifterPatternLo[scanlineSpriteIndex] = spriteShifterPatternLo[scanlineSpriteIndex] << 1 + spriteShifterPatternHi[scanlineSpriteIndex] = spriteShifterPatternHi[scanlineSpriteIndex] << 1 + } + } + } } if scanline == -1 && cycle == 1 { - // This is the start of a new frame, so clear the vertical blank flag. + // This is the start of a new frame, so clear the vertical blank, + // sprite zero hit, and sprite overflow flags. status.remove(.verticalBlank) + status.remove(.spriteZeroHit) + status.remove(.spriteOverflow) + + spriteShifterPatternLo = Array(repeating: 0, count: 8) + spriteShifterPatternHi = Array(repeating: 0, count: 8) } if scanline == -1 @@ -192,6 +219,7 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { } if (-1..<240).contains(scanline) { + // Background rendering. if (2..<258).contains(cycle) || (321..<338).contains(cycle) { updateShifters() @@ -213,14 +241,14 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { case 4: let address = ((control.contains(.patternBackground) ? 1 : 0) << 12) + (UInt16(bgNextTileId) << 4) + vramAddress.fineY + 0 bgNextTileLSB = bus.read(from: mirroredAddress(address)) - + case 6: let address = ((control.contains(.patternBackground) ? 1 : 0) << 12) + (UInt16(bgNextTileId) << 4) + vramAddress.fineY + 8 bgNextTileMSB = bus.read(from: mirroredAddress(address)) - + case 7: incrementScrollX() - + default: break } @@ -240,6 +268,109 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { let address = Self.nametableAddressRange.lowerBound | (vramAddress.rawValue & 0x0fff) bgNextTileId = bus.read(from: mirroredAddress(address)) } + + + // Sprite rendering. + if cycle == 257 && scanline >= 0 { + // Reset list of sprites on this scanline. + scanlineSprites.removeAll(keepingCapacity: true) + + // Reset sprite zero hit flag. + spriteZeroHitPossible = false + + // Loop through each entry in the OAM. + for (index, entry) in objectAttributeMemory.enumerated() { + let diff: Int16 = (Int16(scanline) - Int16(entry.y)) + + // Check if the sprite is within this scanline. + if diff >= 0 && diff < (control.contains(.doubleSpriteHeight) ? 16 : 8) { + // If we haven't added the maximum number of sprites to our `scanlineSprites` array, + // then add the entry to the list of sprites for this scanline. Otherwise, set + // the `.spriteOverflow` status flag. + if scanlineSprites.count < 8 { + scanlineSprites.append(entry) + + // If this is sprite zero, then mark that sprite-0 hit is possible. + if index == 0 { + spriteZeroHitPossible = true + } + } else { + status.insert(.spriteOverflow) + break + } + } + } + } + + if cycle == 340 { + // Reset sprite shifters on this scanline. + spriteShifterPatternLo = Array(repeating: 0, count: 8) + spriteShifterPatternHi = Array(repeating: 0, count: 8) + + for (index, entry) in scanlineSprites.enumerated() { + let spritePatternAddressLo, spritePatternAddressHi: UInt16 + var spritePatternBitsLo, spritePatternBitsHi: UInt8 + + if !control.contains(.doubleSpriteHeight) { + // 8-pixel high sprites. + if (entry.attribute & 0x80) == 0 { + // Sprite NOT flipped vertically. + spritePatternAddressLo = (UInt16(control.contains(.patternSprite) ? 1 : 0) << 12) + | (UInt16(entry.tileId) << 4) + | UInt16(bitPattern: scanline - Int16(entry.y)) + } else { + // Sprite flipped vertically. + spritePatternAddressLo = (UInt16(control.contains(.patternSprite) ? 1 : 0) << 12) + | (UInt16(entry.tileId) << 4) + | (7 &- UInt16(bitPattern: scanline - Int16(entry.y))) + } + } else { + // 16-pixel high sprites. + if (entry.attribute & 0x80) == 0 { + // Sprite NOT flipped vertically. + if scanline - Int16(entry.y) < 8 { + // Top 8 rows of the sprite. + spritePatternAddressLo = ((UInt16(entry.tileId) & 0x01) << 12) + | ((UInt16(entry.tileId) & 0xfe) << 4) + | ((UInt16(bitPattern: scanline - Int16(entry.y))) & 0x07) + } else { + // Bottom 8 rows of the sprite. + spritePatternAddressLo = ((UInt16(entry.tileId) & 0x01) << 12) + | (((UInt16(entry.tileId) & 0xfe) + 1) << 4) + | ((UInt16(bitPattern: scanline - Int16(entry.y))) & 0x07) + } + } else { + // Sprite flipped vertically. + if scanline - Int16(entry.y) < 8 { + // Top 8 rows of the sprite. + spritePatternAddressLo = ((UInt16(entry.tileId) & 0x01) << 12) + | ((UInt16(entry.tileId) & 0xfe) << 4) + | (7 &- ((UInt16(bitPattern: scanline - Int16(entry.y))) & 0x07)) + } else { + // Bottom 8 rows of the sprite. + spritePatternAddressLo = ((UInt16(entry.tileId) & 0x01) << 12) + | (((UInt16(entry.tileId) & 0xfe) + 1) << 4) + | (7 &- ((UInt16(bitPattern: scanline - Int16(entry.y))) & 0x07)) + } + } + } + + // The pattern address for the high bitplane. + spritePatternAddressHi = spritePatternAddressLo + 8 + + // Get the bits of the low and high bitplanes. + spritePatternBitsLo = bus.read(from: spritePatternAddressLo) + spritePatternBitsHi = bus.read(from: spritePatternAddressHi) + + if (entry.attribute & 0x40) > 0 { + spritePatternBitsLo = spritePatternBitsLo.bitSwapped + spritePatternBitsHi = spritePatternBitsHi.bitSwapped + } + + spriteShifterPatternLo[index] = spritePatternBitsLo + spriteShifterPatternHi[index] = spritePatternBitsHi + } + } } if scanline == 241 @@ -267,7 +398,82 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { bgPalette = (paletteHi << 1) | paletteLo } - let bgColor = color(fromPalette: bgPalette, index: bgPixel) + var fgPixel: UInt8 = 0 + var fgPalette: UInt8 = 0 + var fgPriority: UInt8 = 0 + + if mask.contains(.renderSprites) { + spriteZeroBeingRendered = false + + for (index, entry) in scanlineSprites.enumerated() { + if entry.x == 0 { + let pixelLo: UInt8 = (spriteShifterPatternLo[index] & 0x80) > 0 ? 1 : 0 + let pixelHi: UInt8 = (spriteShifterPatternHi[index] & 0x80) > 0 ? 1 : 0 + fgPixel = (pixelHi << 1) | pixelLo + + fgPalette = (entry.attribute & 0x03) + 0x04 + fgPriority = (entry.attribute & 0x20) == 0 ? 1 : 0 + + // If the sprite pixel isn't transparent, then break out as we've found + // a pixel we can (potentially) draw. + if fgPixel != 0 { + // Mark if we're rendering sprite zero. + if index == 0 { + spriteZeroBeingRendered = true + } + + break + } + } + } + } + + var pixel: UInt8 = 0 + var palette: UInt8 = 0 + + if bgPixel == 0 && fgPixel == 0 { + // Background pixel is transparent. + // Sprite pixel is transparent. + // Select the "transparent" color. + pixel = 0x00 + palette = 0x00 + } else if bgPixel == 0 && fgPixel > 0 { + // Background pixel is transparent. + // Sprite pixel is visible. + // Select the sprite pixel. + pixel = fgPixel + palette = fgPalette + } else if bgPixel > 0 && fgPixel == 0 { + // Background pixel is visible. + // Foreground pixel is transparent. + // Select the background pixel. + pixel = bgPixel + palette = bgPalette + } else { + if fgPriority > 0 { + pixel = fgPixel + palette = fgPalette + } else { + pixel = bgPixel + palette = bgPalette + } + + if spriteZeroHitPossible && spriteZeroBeingRendered { + if mask.contains([.renderBackground, .renderSprites]) { + if mask.isDisjoint(with: [.renderBackgroundLeft, .renderSpritesLeft]) { + if cycle >= 9 && cycle < 258 { + status.insert(.spriteZeroHit) + } + } else { + if cycle >= 1 && cycle < 258 { + status.insert(.spriteZeroHit) + } + } + } + } + } + + let bgColor = color(fromPalette: palette, index: pixel) videoReceiver?.setPixel(atX: Int(cycle - 1), y: Int(scanline), withColor: bgColor.rawValue) @@ -317,8 +523,9 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { return data case .oamData: - // TODO: Return OAM data at OAM address. - break + return objectAttributeMemory.withUnsafeBytes { pointer in + pointer[Int(oamAddress)] + } case .ppuData: // Reading data from the PPU is delayed by one cycle. @@ -358,12 +565,12 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { break case .oamAddress: - // TODO: Write the OAM address. - break + oamAddress = value case .oamData: - // TODO: Set the data at the OAM address. - break + objectAttributeMemory.withUnsafeMutableBytes { pointer in + pointer[Int(oamAddress)] = value + } case .scroll: switch ppuAddressLatch { @@ -492,6 +699,13 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { private var bgShiftAttributeLo: UInt16 = 0 private var bgShiftAttributeHi: UInt16 = 0 + private var scanlineSprites: [ObjectAttributeEntry] = [] + private var spriteShifterPatternLo: [UInt8] = [] + private var spriteShifterPatternHi: [UInt8] = [] + + private var spriteZeroHitPossible = false + private var spriteZeroBeingRendered = false + private func mirroredAddress(_ address: Address) -> Address { switch address { @@ -512,7 +726,7 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { case 0x2000...0x23ff, 0x2800...0x2bff: return 0x2000 | (address & 0x03ff) case 0x2400...0x27ff, 0x2c00...0x2fff: - return 0x2800 | (address & 0x0fff) + return 0x2400 | (address & 0x0fff) default: return address } @@ -545,7 +759,7 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { var tileMSB = bus.read(from: UInt16(index) * 0x1000 + byteOffset + UInt16(row) + 8) for column: Int in 0..<8 { - let pixel = (tileLSB & 0x01) + (tileMSB & 0x01) + let pixel = ((tileLSB & 0x01) << 1) | (tileMSB & 0x01) tileLSB >>= 1 tileMSB >>= 1 @@ -565,6 +779,14 @@ final class PixelProcessingUnit: AddressableReadWriteDevice { return Self.colors[colorIndex] } } + +extension PixelProcessingUnit: DirectMemoryAccessableWriteDevice { + func dmaWrite(_ value: Value, to oamAddress: UInt8) { + objectAttributeMemory.withUnsafeMutableBytes { pointer in + pointer[Int(oamAddress)] = value + } + } +} extension PixelProcessingUnit { private struct Color { @@ -643,3 +865,16 @@ extension PixelProcessingUnit { Color(0, 0, 0), Color(0, 0, 0)] } + +extension UInt8 { + /// Returns a "bit-swapped" value. + /// + /// A value of `0b10110000` becomes `0b00001101`. + var bitSwapped: UInt8 { + var b = self + b = ((b & 0xf0) >> 4) | ((b & 0x0f) << 4) + b = ((b & 0xcc) >> 2) | ((b & 0x33) << 2) + b = ((b & 0xaa) >> 1) | ((b & 0x55) << 1) + return b + } +} diff --git a/Sources/SwiftNES/RP2A03G.swift b/Sources/SwiftNES/RP2A03G.swift index ac87f6f..8cfe305 100644 --- a/Sources/SwiftNES/RP2A03G.swift +++ b/Sources/SwiftNES/RP2A03G.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - final class RP2A03G { // MARK: - Initializers diff --git a/Sources/SwiftNES/RandomAccessMemoryDevice.swift b/Sources/SwiftNES/RandomAccessMemoryDevice.swift index 0f82377..36e2f3c 100644 --- a/Sources/SwiftNES/RandomAccessMemoryDevice.swift +++ b/Sources/SwiftNES/RandomAccessMemoryDevice.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// Represents errors thrown by the device. enum RandomAccessMemoryDeviceError: Error { /// The memory size is greater than the address range. diff --git a/Sources/SwiftNES/Types.swift b/Sources/SwiftNES/Types.swift index a7285f1..9c789be 100644 --- a/Sources/SwiftNES/Types.swift +++ b/Sources/SwiftNES/Types.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - /// Represents an address within the addressable range of the NES. public typealias Address = UInt16 diff --git a/Sources/SwiftNES/VideoReceiver.swift b/Sources/SwiftNES/VideoReceiver.swift index 0f8d816..261263c 100644 --- a/Sources/SwiftNES/VideoReceiver.swift +++ b/Sources/SwiftNES/VideoReceiver.swift @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation - public struct VideoOutputParameters { public let resolution: (width: UInt, height: UInt) } diff --git a/Tests/SwiftNESTests/BusTests.swift b/Tests/SwiftNESTests/BusTests.swift index 18a6461..4a024db 100644 --- a/Tests/SwiftNESTests/BusTests.swift +++ b/Tests/SwiftNESTests/BusTests.swift @@ -38,9 +38,4 @@ final class BusTests: XCTestCase { } } } - - static var allTests = [ - ("testReadPerformance", testReadPerformance), - ("testWritePerformance", testWritePerformance), - ] } diff --git a/Tests/SwiftNESTests/DirectMemoryAccessControllerTests.swift b/Tests/SwiftNESTests/DirectMemoryAccessControllerTests.swift new file mode 100644 index 0000000..90ff2aa --- /dev/null +++ b/Tests/SwiftNESTests/DirectMemoryAccessControllerTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import SwiftNES + +final class DirectMemoryAccessControllerTests: XCTestCase { + + class MockReadDevice: DirectMemoryAccessableReadDevice { + func dmaRead(from address: Address) -> Value { + return 0 + } + } + + class MockWriteDevice: DirectMemoryAccessableWriteDevice { + func dmaWrite(_ value: Value, to address: UInt8) { + + } + } + + var dmaController: DirectMemoryAccessController! + var readDevice: MockReadDevice! + var writeDevice: MockWriteDevice! + + override func setUp() async throws { + dmaController = .init(address: 0x1000) + readDevice = .init() + writeDevice = .init() + + dmaController.readDevice = readDevice + dmaController.writeDevice = writeDevice + } +} diff --git a/Tests/SwiftNESTests/SwiftNESTests.swift b/Tests/SwiftNESTests/SwiftNESTests.swift index 109731b..2be21f1 100644 --- a/Tests/SwiftNESTests/SwiftNESTests.swift +++ b/Tests/SwiftNESTests/SwiftNESTests.swift @@ -62,8 +62,4 @@ final class SwiftNESTests: XCTestCase { print(line) } } - - static var allTests = [ - ("testMultiply10By3", testMultiply10By3), - ] }