Skip to content

Commit

Permalink
Adds sprite rendering (#22)
Browse files Browse the repository at this point in the history
- Adds `ObjectAttributeEntry` for sprite entries in OAM.
- Adds `DirectMemoryAccessController` to handle DMA transfers from CPU memory to PPU OAM.
- Updates the PPU to render sprites.
  • Loading branch information
jerrodputman committed Jul 15, 2023
1 parent bc4d4b0 commit a73939e
Show file tree
Hide file tree
Showing 19 changed files with 487 additions and 104 deletions.
2 changes: 0 additions & 2 deletions Sources/SwiftNES/AddressableDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions Sources/SwiftNES/AudioReceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 17 additions & 13 deletions Sources/SwiftNES/Bus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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.

Expand All @@ -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) }
Expand All @@ -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 }
Expand All @@ -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 }
Expand All @@ -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)
}
}
2 changes: 0 additions & 2 deletions Sources/SwiftNES/CartridgeConnector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions Sources/SwiftNES/ControllerConnector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions Sources/SwiftNES/Controllers/ControlPad.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
119 changes: 106 additions & 13 deletions Sources/SwiftNES/DirectMemoryAccessController.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
Loading

0 comments on commit a73939e

Please sign in to comment.