Skip to content

Commit

Permalink
Implements PPU background rendering (#18)
Browse files Browse the repository at this point in the history
* Implements PPU background renderer
* Adds performance tests for Bus and improves performance
* Improves performance of the Bus
  • Loading branch information
jerrodputman committed Jul 9, 2023
1 parent 6b24fce commit d9ba448
Show file tree
Hide file tree
Showing 23 changed files with 1,088 additions and 237 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>BusTests</key>
<dict>
<key>testReadPerformance()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0275</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testWritePerformance()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0281</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>27EA854B-A785-4A10-BB95-9D604E1EEB91</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>0</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>Apple M1</string>
<key>cpuSpeedInMHz</key>
<integer>0</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>8</integer>
<key>modelCode</key>
<string>MacBookAir10,1</string>
<key>physicalCPUCoresPerPackage</key>
<integer>8</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>arm64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone13,3</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
</plist>
2 changes: 1 addition & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/SwiftNES.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1110"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
// swift-tools-version:5.9

import PackageDescription

let package = Package(
name: "SwiftNES",
platforms: [.macOS(.v13)],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
Expand Down
19 changes: 12 additions & 7 deletions Sources/SwiftNES/AddressableDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,34 @@
import Foundation

/// A protocol that describes a device that is addressable on a system bus.
protocol AddressableDevice: class {
protocol AddressableDevice: AnyObject {
/// Returns whether or not the device responds to the specified address.
///
/// - parameter address: The address.
/// - returns: Whether or not the device responds to the address.
/// - Parameters:
/// - address: The address.
/// - Returns: Whether or not the device responds to the address.
func respondsTo(_ address: Address) -> Bool

var addressRange: AddressRange { get }
}

/// A protocol that describes a device that is addressable on a system bus and can be read from.
protocol AddressableReadDevice: AddressableDevice {
/// Reads from the addressable device at the specified address.
///
/// - parameter address: The address to read from.
/// - returns: The value stored at the address.
/// - Parameters:
/// - address: The address to read from.
/// - Returns: The value stored at the address.
func read(from address: Address) -> Value
}

/// A protocol that describes a device that is addressable on a system bus and can be written to.
protocol AddressableWriteDevice: AddressableDevice {
/// Writes data to the addressable device at the specified address.
///
/// - parameter data: The data to be written.
/// - parameter address: The address to write to.
/// - Parameters:
/// - data: The data to be written.
/// - address: The address to write to.
func write(_ data: Value, to address: Address)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftNES/AudioReceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import Foundation

public protocol AudioReceiver: class {
public protocol AudioReceiver: AnyObject {
// TODO: Define an AudioReceiver.
// TODO: Move protocol into an SDK package.
}
80 changes: 56 additions & 24 deletions Sources/SwiftNES/Bus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,47 +27,79 @@ final class Bus {

// MARK: - Initializers

/// Creates a bus with attached devices.
/// Initializes a bus with attached devices.
///
/// - parameter addressableDevices: The devices that can be addressed via this bus.
init(addressableDevices: [AddressableDevice]) {
self.addressableReadDevices = addressableDevices.compactMap { $0 as? AddressableReadDevice }
self.addressableWriteDevices = addressableDevices.compactMap { $0 as? AddressableWriteDevice }
/// - Parameters:
/// - addressableDevices: The devices that can be addressed via this bus.
init(addressableDevices: [any AddressableDevice]) throws {
// TODO: Verify that there are no overlapping devices.

let addressableReadDevices = addressableDevices.compactMap { $0 as? AddressableReadDevice }
let addressableWriteDevices = addressableDevices.compactMap { $0 as? AddressableWriteDevice }

self.addressableReadDeviceRanges = addressableReadDevices.map(\.addressRange)
self.addressableWriteDeviceRanges = addressableWriteDevices.map(\.addressRange)
self.addressableReadDevices = addressableReadDevices
self.addressableWriteDevices = addressableWriteDevices
}


// MARK: - Reading and writing

/// Reads data from a device on the 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.
subscript(address: Address) -> Value {
get { read(from: address) }
set { write(newValue, to: address) }
}

/// Reads data from the specified address on the bus.
///
/// - note: If a device does not respond to the address, `0` will be returned.
/// - Note: If a device does not respond to the address, `0` will be returned.
///
/// - parameter address: The address to read from.
/// - returns: The value that was read from a device on the bus.
/// - Parameters:
/// - 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 deviceToReadFrom = addressableReadDevices
.first(where: { $0.respondsTo(address) }) else { return 0 }
guard let deviceToReadFromIndex = addressableReadDeviceRanges
.firstIndex(where: { $0.contains(address) }) else { return 0 }

let deviceToReadFrom = addressableReadDevices[deviceToReadFromIndex]

return deviceToReadFrom.read(from: address)
}
/// Writes data to a device on the bus.

/// Writes data to the specified address on the bus.
///
/// - note: If a device does not respond to the address, this method does nothing.
/// - Note: If a device does not respond to the address, this method does nothing.
///
/// - parameter value: The value to write to the bus.
/// - parameter address: The address to write to.
/// - Parameters:
/// - value: The value to write to the bus.
/// - address: The address to write to.
func write(_ value: Value, to address: Address) {
guard let deviceToWriteTo = addressableWriteDevices
.first(where: { $0.respondsTo(address) }) else { return }

guard let deviceToWriteToIndex = addressableWriteDeviceRanges
.firstIndex(where: { $0.contains(address) }) else { return }

let deviceToWriteTo = addressableWriteDevices[deviceToWriteToIndex]

deviceToWriteTo.write(value, to: address)
}


// MARK: - Private

/// All attached devices on the bus.
private let addressableReadDevices: [AddressableReadDevice]
private let addressableWriteDevices: [AddressableWriteDevice]
/// The ranges of the addressable devices attached to the bus that can be read from.
private let addressableReadDeviceRanges: [AddressRange]

/// The ranges of the addressable devices attached to the bus that can be written to.
private let addressableWriteDeviceRanges: [AddressRange]

/// All of the addressable devices attached to the bus that can be read from.
private let addressableReadDevices: [any AddressableReadDevice]

/// All of the addressable devices attached to the bus that can be written to.
private let addressableWriteDevices: [any AddressableWriteDevice]
}
20 changes: 17 additions & 3 deletions Sources/SwiftNES/Cartridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import Foundation

/// An enumeration that defines the types of errors a cartridge can throw.
enum CartridgeError: Error {
public enum CartridgeError: Error {
/// The data was not in the `iNES` format.
case invalidDataFormat

Expand All @@ -44,7 +44,7 @@ public final class Cartridge {
/// - parameter programStartAddress: Forces the starting address to a specified value.
/// This is typically only used for testing purposes, as all cartridges should have their program start
/// address set.
init(data: Data, programStartAddress: Address? = nil) throws {
public init(data: Data, programStartAddress: Address? = nil) throws {
var dataLocation = 0

// Read the 16-byte header.
Expand Down Expand Up @@ -88,6 +88,9 @@ public final class Cartridge {
// Create and store the mapper.
mapper = try mapperType.init(programMemoryBanks: programMemoryBanks, characterMemoryBanks: characterMemoryBanks)

// Determine the mirroring mode.
mirroringMode = (header.mapper1 & 0x01) > 0 ? .vertical : .horizontal

// If a program start address was specified, write it to the cartridge.
if let programStartAddress = programStartAddress {
write(UInt8(programStartAddress & 0x00ff), to: 0xfffc)
Expand All @@ -99,10 +102,11 @@ public final class Cartridge {
///
/// - parameter string: A string containing program code. The program code must be compiled object
/// code in hexadecimal format.
init(string: String) throws {
public init(string: String) throws {
programMemory = [Value](repeating: 0, count: 0x4000)
characterMemory = []
mapper = try Self.mapperTypes[0]!.init(programMemoryBanks: 1, characterMemoryBanks: 1)
mirroringMode = .horizontal

let programCode = string.hexToUInt8
programMemory.replaceSubrange(0..<programCode.count, with: programCode)
Expand All @@ -111,6 +115,16 @@ public final class Cartridge {
}


// MARK: - Accessing information about the cartridge

enum MirroringMode {
case horizontal
case vertical
}

let mirroringMode: MirroringMode


// MARK: - Reading and writing

/// Reads from the specified address in the cartridge.
Expand Down
12 changes: 5 additions & 7 deletions Sources/SwiftNES/CartridgeConnector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ final class CartridgeConnector: AddressableReadWriteDevice {

// MARK: - AddressableReadWriteDevice

@inlinable
func respondsTo(_ address: Address) -> Bool {
return addressRange.contains(address)
addressRange.contains(address)
}

/// The range of addresses that this connector responds to.
let addressRange: AddressRange

func read(from address: Address) -> Value {
guard respondsTo(address) else { return 0 }

Expand All @@ -61,10 +65,4 @@ final class CartridgeConnector: AddressableReadWriteDevice {

cartridge?.write(value, to: address)
}


// MARK: - Private

/// The range of addresses that this connector responds to.
private let addressRange: AddressRange
}
45 changes: 45 additions & 0 deletions Sources/SwiftNES/DirectMemoryAccessController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// MIT License
//
// Copyright (c) 2020 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.

import Foundation

struct ObjectAttributeEntry {
var y: UInt8
var id: UInt8
var attribute: UInt8
var x: UInt8
}

var objectAttributeMemory: [ObjectAttributeEntry] = []

final class DirectMemoryAccessController {

init() {
page = 0
address = 0
data = 0
}

private var page: UInt8
private var address: UInt8
private var data: Value
}
Loading

0 comments on commit d9ba448

Please sign in to comment.