Skip to content

Commit

Permalink
Adds Mapper002
Browse files Browse the repository at this point in the history
- Adds `Mapper002` implementation.
- Updates `Cartridge` implementation  to handle more complex mappers.
- Adds tests for `Mapper000` and `Mapper002`.
  • Loading branch information
jerrodputman committed Jul 16, 2023
1 parent 1764a1f commit 3a56f1c
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 70 deletions.
61 changes: 46 additions & 15 deletions Sources/SwiftNES/Cartridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,19 @@ public final class Cartridge {
dataLocation += programMemorySize

let characterMemoryBanks = header.chrRomChunks
let characterMemorySize = 0x2000 * Int(characterMemoryBanks)
if characterMemoryBanks > 0 {
let characterMemorySize = 0x2000 * Int(characterMemoryBanks)

guard data.count >= dataLocation + characterMemorySize else {
throw CartridgeError.invalidDataFormat
guard data.count >= dataLocation + characterMemorySize else {
throw CartridgeError.invalidDataFormat
}

characterMemory = [Value](data[dataLocation..<dataLocation + characterMemorySize])
dataLocation += characterMemorySize
} else {
// No character ROM banks, so set up character memory as RAM.
characterMemory = Array(repeating: 0, count: 0x2000)
}

characterMemory = [Value](data[dataLocation..<dataLocation + characterMemorySize])
dataLocation += characterMemorySize

// Extract the mapper ID.
let mapperId = ((header.mapper2 >> 4) << 4) | (header.mapper1 >> 4)
Expand All @@ -89,7 +94,7 @@ public final class Cartridge {
mapper = try mapperType.init(programMemoryBanks: programMemoryBanks, characterMemoryBanks: characterMemoryBanks)

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

// If a program start address was specified, write it to the cartridge.
if let programStartAddress = programStartAddress {
Expand All @@ -106,7 +111,7 @@ public final class Cartridge {
programMemory = [Value](repeating: 0, count: 0x4000)
characterMemory = []
mapper = try Self.mapperTypes[0]!.init(programMemoryBanks: 1, characterMemoryBanks: 1)
mirroringMode = .horizontal
hardwareMirroringMode = .horizontal

let programCode = string.hexToUInt8
programMemory.replaceSubrange(0..<programCode.count, with: programCode)
Expand All @@ -117,12 +122,24 @@ public final class Cartridge {

// MARK: - Accessing information about the cartridge

/// The mirroring modes that a cartridge can employ.
enum MirroringMode {
/// Horizontal mirroring.
case horizontal

/// Vertical mirroring.
case vertical
}

let mirroringMode: MirroringMode
/// The current mirroring mode.
// TODO: Mirroring actually occurs on the cartridge and not in the PPU.
var mirroringMode: MirroringMode {
if let mirroringMode = mapper.mirroringMode {
return mirroringMode
} else {
return hardwareMirroringMode
}
}


// MARK: - Reading and writing
Expand All @@ -132,13 +149,15 @@ public final class Cartridge {
/// - parameter address: The address to read from. This address will be mapped by the `mapper`.
/// - returns: The data at the specified address.
func read(from address: Address) -> Value {
let mappedAddress = mapper.map(address)
let mappedAddress = mapper.read(from: address)

switch mappedAddress {
case .program(let address):
return programMemory[Int(address)]
case .character(let address):
return characterMemory[Int(address)]
case .value(let value):
return value
case .none:
return 0
}
Expand All @@ -149,18 +168,27 @@ public final class Cartridge {
/// - parameter value: The value to write.
/// - parameter address: The address to write to. This address will be mapped by the `mapper`.
func write(_ value: Value, to address: Address) {
let mappedAddress = mapper.map(address)
let mappedAddress = mapper.write(value, to: address)

switch mappedAddress {
case .program(let address):
programMemory[Int(address)] = value
case .character(let address):
characterMemory[Int(address)] = value
case .none:
case .value, .none:
break
}
}

// MARK: Resetting the cartridge

/// Resets the cartridge.
///
/// This is primarily used to reset the mapper inside of the cartridge.
func reset() {
mapper.reset()
}


// MARK: - Private

Expand All @@ -171,13 +199,16 @@ public final class Cartridge {
private var characterMemory: [Value]

/// The catridge's mapper.
private let mapper: Mapper
private let mapper: any Mapper

/// The mirror mode set by the cartridge hardware. Can be overridden by the mapper.
private var hardwareMirroringMode: MirroringMode


/// The types of mappers that are found in a cartridge.
// TODO: If all of the mapper types are ever implemented, this should be an array.
private static let mapperTypes: [UInt8: Mapper.Type] = [
0: Mapper000.self
0: Mapper000.self,
2: Mapper002.self,
]


Expand Down
78 changes: 36 additions & 42 deletions Sources/SwiftNES/Mapper.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// MIT License
//
// Copyright (c) 2019 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,16 +20,19 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

/// An enumeration that defines the mapped addresses that can be returned by a mapper.
enum MappedAddress {
/// The address could not be mapped.
case none
/// An enumeration that defines the result of a mapper operation.
enum MapperResult: Equatable {
/// The mapper returned an offset into program memory.
case program(UInt32)

/// The mapper returned an offset into character memory.
case character(UInt32)

/// An address in program memory.
case program(Address)
/// The mapper returned a value.
case value(Value)

/// An address in character memory.
case character(Address)
/// The mapper did not return a result.
case none
}

/// An enumeration that defines common errors that can be thrown by mappers.
Expand All @@ -44,43 +47,34 @@ enum MapperError: Error {
protocol Mapper {
/// Creates the mapper with the specified number of program and character memory banks of the cartridge.
///
/// - parameter programMemoryBanks: The number of program memory banks that the cartridge has.
/// - parameter characterMemoryBanks: The number of character memory banks that the cartridge has.
/// - Parameter programMemoryBanks: The number of program memory banks that the cartridge has.
/// - Parameter characterMemoryBanks: The number of character memory banks that the cartridge has.
init(programMemoryBanks: UInt8, characterMemoryBanks: UInt8) throws

/// Maps the specified address into an address in either program or character memory.
///
/// - parameter address: The address to map.
/// - returns: The mapped address.
func map(_ address: Address) -> MappedAddress
}

extension Mapper {
/// Whether or not the mapper responds to this address.
/// Maps the address to be read.
///
/// - parameter address: The address.
/// - returns: Whether or not the mapper responds to the address.
@inlinable
func respondsTo(_ address: Address) -> Bool {
return isAddressingProgramMemory(address)
|| isAddressingCharacterMemory(address)
}
/// - Parameter address: The ``Address`` to be read.
/// - Returns: The ``MappedResult`` of the read.
func read(from address: Address) -> MapperResult

/// Returns whether or not the specified address is addressing program memory.
/// Maps the address to be written to.
///
/// - parameter address: The address.
/// - returns: Whether or not the address is addressing program memory.
@inlinable
func isAddressingProgramMemory(_ address: Address) -> Bool {
return (0x8000...0xffff).contains(address)
}
/// - Parameter data: The ``Value`` to be written to the address.
/// - Parameter address: The ``Address`` to be written to.
/// - Returns: The ``MappedResult`` of the write.
func write(_ data: Value, to address: Address) -> MapperResult

/// Returns whether or not the specified address is addressing character memory.
///
/// - parameter address: The address.
/// - returns: Whether or not the address is addressing character memory.
@inlinable
func isAddressingCharacterMemory(_ address: Address) -> Bool {
return (0x0000...0x1fff).contains(address)
}
/// Resets the mapper.
func reset()

/// The mirroring mode returned by the mapper.
var mirroringMode: Cartridge.MirroringMode? { get }
}

extension Mapper {
/// The ``AddressRange`` of program memory in a ``Cartridge``.
static var programMemoryAddressRange: AddressRange { 0x8000...0xffff }

/// The ``AddressRange`` of character memory in a ``Cartridge``.
static var characterMemoryAddressRange: AddressRange { 0x0000...0x1fff }
}
35 changes: 22 additions & 13 deletions Sources/SwiftNES/Mappers/Mapper000.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// MIT License
//
// Copyright (c) 2019 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,15 +20,13 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation

final class Mapper000: Mapper {

// MARK: - Mapper

required init(programMemoryBanks: UInt8, characterMemoryBanks: UInt8) throws {
// This mapper only allows 1 or 2 program memory banks.
guard (1...2).contains(programMemoryBanks) else {
guard 1...2 ~= programMemoryBanks else {
throw MapperError.invalidNumberOfProgramMemoryBanks(programMemoryBanks)
}

Expand All @@ -40,18 +38,29 @@ final class Mapper000: Mapper {
self.programMemoryBanks = programMemoryBanks
// Character memory bank switching not supported by this mapper.
}

func map(_ address: Address) -> MappedAddress {
if isAddressingProgramMemory(address) {
// For a single bank, the 16 KB memory is mirrored across the second bank.
let mappedAddress = address & ((programMemoryBanks > 1) ? 0x7fff : 0x3fff)
return .program(mappedAddress)
} else if isAddressingCharacterMemory(address) {
return .character(address)
} else {


func read(from address: Address) -> MapperResult {
switch address {
case Self.programMemoryAddressRange:
let mappedAddress = address & (programMemoryBanks > 1 ? 0x7fff : 0x3fff)
return .program(UInt32(mappedAddress))
case Self.characterMemoryAddressRange:
return .character(UInt32(address))
default:
return .none
}
}

func write(_ data: Value, to address: Address) -> MapperResult {
return .none
}

func reset() {
// Do nothing.
}

var mirroringMode: Cartridge.MirroringMode? { nil }


// MARK: - Private
Expand Down
86 changes: 86 additions & 0 deletions Sources/SwiftNES/Mappers/Mapper002.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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.

final class Mapper002: Mapper {
init(programMemoryBanks: UInt8, characterMemoryBanks: UInt8) throws {
// This mapper does not allow zero program memory banks.
guard 1...255 ~= programMemoryBanks else {
throw MapperError.invalidNumberOfProgramMemoryBanks(programMemoryBanks)
}

// This mapper only allows 0 or 1 character memory bank.
guard 0...1 ~= characterMemoryBanks else {
throw MapperError.invalidNumberOfCharacterMemoryBanks(characterMemoryBanks)
}

self.programMemoryBanks = programMemoryBanks
self.characterMemoryBanks = characterMemoryBanks
}

func read(from address: Address) -> MapperResult {
switch address {
case 0x8000...0xbfff:
return .program(UInt32(bankSelectLo) * 0x4000 + (UInt32(address) & 0x3fff))
case 0xc000...0xffff:
return .program(UInt32(bankSelectHi) * 0x4000 + (UInt32(address) & 0x3fff))
case Self.characterMemoryAddressRange:
return .character(UInt32(address))
default:
return .none
}
}

func write(_ data: Value, to address: Address) -> MapperResult {
switch address {
case Self.programMemoryAddressRange:
bankSelectLo = (data & 0x0f)
return .none
case Self.characterMemoryAddressRange where characterMemoryBanks == 0:
// Use as RAM if memory banks are 0.
return .character(UInt32(address))
default:
return .none
}
}

func reset() {
bankSelectLo = 0
bankSelectHi = programMemoryBanks - 1
}

var mirroringMode: Cartridge.MirroringMode? { nil }


// MARK: - Private

/// The number of program memory banks that the cartridge has.
private let programMemoryBanks: UInt8

/// The number of character memory banks that the cartridge has.
private let characterMemoryBanks: UInt8

/// The selected low program memory bank.
private var bankSelectLo: UInt8 = 0x00

/// The selected high program memory bank.
private var bankSelectHi: UInt8 = 0x00
}
1 change: 1 addition & 0 deletions Sources/SwiftNES/NES.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public final class NES {
videoReceiver?.setVideoOutputParameters(videoOutputParams)

cpu.core.reset()
cartridge?.reset()
}


Expand Down
Loading

0 comments on commit 3a56f1c

Please sign in to comment.