Newshot

一个用 Odin 编写的 NES 模拟器

Translation(model translation, not the source's own text)

我用 Odin 写了一个 NES 模拟器!

lucyNES
lucyNES

简介

我在 2024 年 6 月下旬开始了这个项目。我利用业余时间断断续续地做。2025 年 1 月,我决定暂停这个项目,专注于其他事情。

我学到了很多关于以下方面的知识:

  • NES 架构,以及可能关于游戏机架构的通用知识
  • 位和字节、十六进制表示法以及位运算。
  • Odin 编程语言。我之前没有在正式项目中使用过它。
  • 计算机如何工作。CPU 指令、“周期”等
  • 模拟。这非常困难。向模拟器开发者致敬!

这是一段狂野的旅程。我获得了很多乐趣,但在接近尾声、试图让模拟器“完全”准确时,我也遭受了更多痛苦。

你可以阅读 Github Readme 了解更多关于该项目的信息。

特性

  • 足够的模拟精度,可以完美运行 Battletoads 和数百款其他游戏。
  • 带有一些调试功能的调试器视图
  • 用于与模拟器交互的菜单视图
  • 手柄支持
  • CRT 着色器开关
  • (部分)声音
  • 支持的 Mapper:NROM、MMC1、UXROM、CNROM、MMC3、GxROM、AxROM

制作模拟器

我见过一些人说,制作一个 NES 模拟器是一个简单的周末项目,坦白说,我觉得这令人费解。因为很明显,事实并非如此。我花了 7 个月才让我的模拟器达到现在的状态。

我认为他们实际上的意思是,让一个 NES 模拟器“跑起来”只需要很短的时间。比如也许能让 Donkey Kong 运行。这是真的。你可以很快达到 80% 甚至 90%。是最后 10% 需要很长、很长的时间。

NES 硬件有许多特性,需要模拟这些特性才能让某些游戏运行。许多游戏依赖于非常特定的 NES 行为。如果你没有正确实现它,它们就不会运行。很多挑战都与时序有关。CPU 和 PPU(NES 中处理图形渲染的芯片)并行运行,并且相互通信。这个时序必须非常准确,否则可能会发生许多错误。大多数游戏在不太准确的模拟器上也能正常运行,但很多游戏不行,它们需要你更详细地考虑问题。

另一个问题是 mapper。Mapper 是我们对一种卡带实现方式的称呼。你看,NES 游戏以卡带形式出现,而卡带不仅仅是数据。它们是具有自身行为以及额外资源(如只读存储器和可写存储器)的电路。有些还带有额外的音频能力,但我没有实现那个。

总之,如果你想让某个卡带中的游戏在你的游戏中运行,你就必须实现这些 mapper。它们完善了 NES 的电路。在某种程度上,它们是 NES 硬件的一部分。有很多 mapper,但只实现少数几个就能支持很多游戏。我在实现一些 mapper 时遇到了困难,其中一些仍然实现得很糟糕,导致一些游戏无法运行。我发现有些 mapper 文档很难理解。

我认为制作模拟器最大的挑战是知道要实现什么。关于硬件的信息,以及为了让游戏在你的模拟器中运行,需要模拟硬件的哪些方面。值得庆幸的是,NES 在网上有非常完善的文档。我使用了许多资源,例如:

  • Nesdev wiki
  • nesdev wiki 的 discord。这里的人们在很多问题上帮助了我。
  • Javidx 的 NES 系列。代码有很多错误,但它是一个很好的起点。
  • 这篇关于 NES 的 PPU 的文章
  • 测试 ROM。这些 NES ROM 会彻底测试你模拟器的特定方面。这是加速模拟器精度开发的一个非凡工具。

如果没有这些资源以及 nesdev discord 中人们的帮助,我无法完成任何这些工作。

Odin

用 Odin 编写的 NES 模拟器

在做这个项目的过程中,我对 Odin 有了很多了解。我觉得它和这个项目非常契合。它是一种非常简单、强类型的类 C 语言,给了我想要的全部自由,我感觉这让编写模拟器比使用其他语言容易得多。

有一件事让我措手不及:Odin 没有自动整数提升。在对数字进行计算时,Odin 绝不会把类型提升为更大的类型。这导致了一些 bug。但我把这全归咎于我自己,以及我习惯了 C 的整数提升。

我学到的一件有趣的事是稍微做一点内存分配策略。Odin 没有自动内存管理,所以你必须想出聪明的方式来处理内存。思考如何最好地处理内存让我觉得很有趣。我不需要做任何复杂的事情。我主要使用一个每帧都会清空的临时分配器、一个用于需要永久存在的东西的全局堆分配器,以及一个用于任何需要在一个 NES 会话期间存在的东西的 NES Arena 分配器。我觉得内存分配很有趣。

我还必须学会的另一件事是如何序列化 Odin 对象。我使用了 CBOR 库,并遇到了一些问题。我以为是我的代码因为库里的一个 bug 而表现异常,但后来澄清了。它出问题只是因为我做了一些它不知道如何序列化的奇怪类型。一旦我学会如何正确地为类型编写自定义处理,一切就完美运行了。这是 cbor 不知道如何序列化的棘手类型之一:


        PpuCtrl :: struct #raw_union {
	// VPHB SINN
	using flags: bit_field u8 {
		n: u8 | 2,
		i: u8 | 1,
		s: u8 | 1, // sprite pattern table address for 8x8 sprites (0: $0000, 1: $1000)
		b: u8 | 1, // bg pattern table address (0: $0000, 1: $1000)
		h: u8 | 1, // sprite size (0: 8x8, 1: 8x16)
		p: u8 | 1,
		v: u8 | 1, // vblank NMI enable
	},
	reg:         u8,
}
      

我很喜欢 Odin 让你像这样定义原始联合体,并且还能带位字段。它非常适合模拟器开发。

还缺什么

我不认为这个项目“完成了”。当然,这样的项目永远不会真正完成,但它的很多方面肯定还需要大量工作:

  • 声音。lucyNES 听起来仍然不是很好。有些游戏听起来还行,但大多数不行。我必须改进实现,并实现我还没做的 DMC 声道。
  • 我测试过的一些游戏因为 mapper 实现不佳而无法运行
  • 我可以实现更多 mapper,从而支持更多游戏。

这些是目前主要缺少的东西。也许在某个时候我会回来继续做这个,让这个项目感觉“完整”。

展示

调试器展示

菜单展示

游戏展示

展示图片
展示图片
展示图片
展示图片
展示图片
展示图片
展示图片
展示图片
展示图片
展示图片
展示图片
展示图片

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Lobsters
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗