去年夏天,我受邀参加了桑尼维尔的一个聚会。 原来,车库里的车主有一个街机机NBA JAM锦标赛版,可容纳四名球员。 尽管该游戏已有25年历史了(它于1993年发布),但玩起来还是很有趣的,特别是对于狂热的粉丝。
我对不包括迈克尔·乔丹的芝加哥公牛球员名单感到惊讶。 根据消息来源
[1] ,MJ获得了自己的许可,并不属于Midway与NBA达成的交易的一部分。
在询问机器所有者之后,我发现黑客为SNES发布了一个“ NBA Jam 2K17”游戏机,可以让新玩家和MJ玩游戏,但没人知道街机版本的工作方式。 因此,我绝对必须查看内部。
背景知识
NBA Jam的故事并非始于篮球,而是始于Jean-Claude Van Damme。 在《环球士兵》发行的大约同一时间,Midway Games开发了技术来操纵大型的,数字化的,真实感逼真的精灵,并保留了与真实演员的相似之处。 这是一项巨大的技术突破:每秒60帧的动画,以前看不见的100x100像素大小的精灵,每个精灵都有自己的256色调色板。
该公司在流行的射击游戏“终结者2:审判日”
[2]中成功使用了该技术,但无法获得“通用士兵”的许可(JCVD的财务状况对Midway来说是不可接受的
[3] )。 当谈判以失败告终时,Midway改变了方向,并开始开发1991年Capcom畅销的格斗游戏,名为Street Fighter II:The World Warrior。
由四人组成的团队(埃德·布恩(Ed Boone)编写了代码,约翰·托比亚斯(John Tobias)做艺术和剧本,约翰·沃格尔(John Vogel)绘制了图形,丹·弗登(Dan Forden)是一名音响工程师)。 经过一年的努力
[4], Midway于1992年推出了Mortal Kombat。
视觉风格与通常的像素艺术大不相同,而且游戏的设计至少可以说是“有争议的”。 屏幕上有几升鲜血和疯狂残酷的推动-“致命性”的游戏立刻风靡全球,一年就赚了将近10亿美元
[5] 。
SF2:384×224,具有4,096色。MK:400×254,具有32,768色。有趣的事实:像在PC上的VGA模式0x13中,在这些游戏中像素不是正方形的。 尽管真人快打帧缓冲区的大小为400×254,但可以将其拉伸到CRT屏幕的4:3的比例,从而提供400×300的分辨率
[6]中途T单元设备
Midway为真人快打开发的硬件非常好。 太好了,他被赋予了自己的名字T-Unit,并在其他游戏中重复使用。
- 真人快打。
- 真人快打II。
- NBA果酱。
- NBA Jam Tournament Edition。
- 德雷德法官(尚未被释放)。
T-Unit由两个板组成。 他们中的大多数涉及游戏逻辑和图形。
NBA JAM TE Edition处理器板(约40x40厘米或15英寸)。另一块板不那么复杂,但也有很多功能。 它是为音频而设计的,但它不仅可以播放使用FM合成的音乐,还可以播放数字声音。
声卡已连接至电源,并且图形卡安装在背面。 请注意位于左上角的巨大散热器。
这两个板一起包含超过200个芯片,电阻器和EPROM。 仅基于序列号来了解所有这些信息将非常耗时。 但是,令人惊讶的是,有时会偶然发现90年代文档中的设备。 而就NBA Jam而言,她就很棒。
中途T单元架构
在寻找数据时,我遇到了一个NBA Jam Kit。 该文档的详细程度令人惊讶
[7] 。 除其他外,我设法找到了包括EPROM和芯片在内的接线的详细描述。
文档中的信息使我们能够绘制电路板图并确定每个零件的功能。 为了帮助搜索零件,电路板的坐标以右下角(UA0)开始,并增加到左上角(UJ26)。
主板的核心是德州仪器(TI)TMS34010(UB21),其频率为50 MHz,EPROM中具有1兆字节代码,而512 kb DRAM
[8] 。 34010是带有16位总线的32位芯片,它具有PIXT和PIXBLT
[9]这样出色的图形指令。 在90年代初期,该芯片被用于多种硬件加速卡
[10] ,我认为它可以处理大量的图形效果。 出人意料的是,他只处理游戏逻辑,却一无所获。
实际上,被称为“ DMA2”的UE13芯片原来是一个图形怪兽。 根据文档中的图表,它当时具有令人印象深刻的32位数据总线和32位地址总线,这就是为什么它成为板上最大的芯片。 这种专用集成电路(ASIC)可以执行许多图形操作,下面将对此进行讨论。
所有芯片(系统RAM,GFX EPROM,调色板SDRAM,代码,视频库)都映射到一个32位地址空间,并连接到同一条总线。 我找不到有关总线协议的任何信息,因此,如果您知道有关总线协议的任何信息,请写信给电子邮件。
请注意一个技巧:一个EPROM组件(标记为蓝色)用于创建另一个存储系统(并节省金钱)。 这些512 kb EPROM具有32位地址引脚和8位数据引脚。 对于需要16位数据总线的34010,两个EPROM(J12和G12)以地址的双重交替连接,从而创建了1兆字节的存储器。 类似地,图形资源与地址的四倍交替连接以形成32位地址,其中32位存储系统包含8兆字节。
尽管在本文中我将主要考虑图形管道,但是我无法抗拒这种诱惑,因此我将简要讨论音频系统。
声卡图显示了Motorola 6809(频率为2 MHz的U4),它从一个EPROM(U3)接收指令以控制音乐和声音效果。
Yamaha的FM合成芯片2151(3.5 MHz)直接根据从6809接收到的指令生成音乐(音乐使用相当小的带宽)。
OKI6295(1 MHz)负责播放ADPCM格式的数字音频(例如,传奇的“ Boomshakalaka”
[11] Tim Kittsrow)。
请注意,在主板上,相同的蓝色512 KB EPROM 32a / 8d用在16位系统中,该地址具有用于存储数字化语音的地址的双重交织,但对于8位指令,则不会交织Motorola 6809的数据/地址。
镜框寿命
整个NBA Jam屏幕均以16位调色板索引。 颜色以xRGB 1555格式存储在64 KB调色板中。 调色板分为512个字节的128个块(256 * 16位)。 存储在EPROM中的精灵被标记为“ GFX”。 每个子画面都有其自己的调色板,最多256x16位颜色。 子画面通常使用整个调色板块,但不超过一个。 使用RAMDAC将CRT信号传输到监视器,该监视器对每个像素从Video DRAM库读取索引,并在调色板中执行颜色搜索。
NBA Jam视频每一帧的寿命如下:
- 游戏逻辑包含从J12 / G12传输到34010的16位指令流。
- 34010读取玩家输入,计算游戏状态,然后绘制屏幕。
- 为了在屏幕上绘制,34010首先在调色板中找到一个未使用的块,并将子画面调色板写入其中(子画面调色板与指令34010一起存储在J12 / G12中)。
- 34010向DMA2发出请求,其中包括精灵的地址和大小,使用的8位调色板块,截断,缩放,处理透明像素的方法等。
- DMA2从J14-G23 GFX ROM芯片读取8位Sprite索引,将该值与8位调色板块的索引合并,然后将16位索引写入视频存储区。 可以将DRAM2视为从GFX EPROM读取8位值并将16位值写入视频存储区的缓冲器
- 重复步骤3-5,直到完成所有对绘制精灵的请求为止。
- 当涉及屏幕刷新时,RAMDAC将视频库中的数据转换成CRT监视器可以理解的信号。 为了使带宽足以将16位索引转换为16位RGB,必须将调色板存储在非常昂贵且非常快的SRAM中。
一个有趣的事实: EPROM闪存固件不是一个简单的过程。 在写入芯片之前,必须完全擦除其所有内容。
为此,必须用紫外线照射芯片。 首先,您需要从EPROM的顶部上揭下标签以打开其图。 然后将EPROM放置在特殊的橡皮擦设备中,该设备中装有紫外线灯。
20分钟后,EPROM将充满零并准备进行记录。
MAME文档
弄清楚设备后,我意识到您可以写给Michael Jordan哪套EPROM(调色板存储在Code EPROM中,而索引存储在GFX EPROM中)。 但是,我仍然不知道使用的确切位置或格式。
MAME中找不到文档。
如果您不知道这个惊人的模拟器是如何工作的,我将简要说明。 MAME基于“驱动程序”的概念,它是对板的模仿。 每个驱动器由(通常)模拟每个芯片的组件组成。 对于Midway T单元,我们对以下文件感兴趣:
妈妈/包括/ midtunit.h
mame / src / mame / video / midtunit.cpp
mame / src / mame /驱动程序/ midtunit.cpp
mame / src / mame /机器/ midtunit.cpp
cpu / tms34010 / tms34010.h
如果查看drivers / midtunit.cpp,我们将看到每个内存芯片都是单个32位地址空间的一部分。 从驱动程序源代码可以看出,调色板从0x01800000开始,gfxrom从0x02000000开始,而DMA2芯片从0x01a80000开始。 要遵循数据路径,我们需要遵循当读或写操作的对象是内存地址时执行的C ++函数。
void midtunit_state::main_map(address_map &map) { map.unmap_value_high(); map(0x00000000, 0x003fffff).rw(m_video, FUNC(midtunit_vram_r), FUNC(midtunit_vram_w)); map(0x01000000, 0x013fffff).ram(); map(0x01400000, 0x0141ffff).rw(FUNC(midtunit_cmos_r), FUNC(midtunit_cmos_w)).share("nvram"); map(0x01480000, 0x014fffff).w(FUNC(midtunit_cmos_enable_w)); map(0x01600000, 0x0160000f).portr("IN0"); map(0x01600010, 0x0160001f).portr("IN1"); map(0x01600020, 0x0160002f).portr("IN2"); map(0x01600030, 0x0160003f).portr("DSW"); map(0x01800000, 0x0187ffff).ram().w(m_palette, FUNC(write16)).share("palette"); map(0x01a80000, 0x01a800ff).rw(m_video, FUNC(midtunit_dma_r), FUNC(midtunit_dma_w)); map(0x01b00000, 0x01b0001f).w(m_video, FUNC(midtunit_control_w)); map(0x01d00000, 0x01d0001f).r(FUNC(midtunit_sound_state_r)); map(0x01d01020, 0x01d0103f).rw(FUNC(midtunit_sound_r), FUNC(midtunit_sound_w)); map(0x01d81060, 0x01d8107f).w("watchdog", FUNC(watchdog_timer_device::reset16_w)); map(0x01f00000, 0x01f0001f).w(m_video, FUNC(midtunit_control_w)); map(0x02000000, 0x07ffffff).r(m_video, FUNC(midtunit_gfxrom_r)).share("gfxrom"); map(0x1f800000, 0x1fffffff).rom().region("maincpu", 0); map(0xff800000, 0xffffffff).rom().region("maincpu", 0); }
在同一文件“ drivers / midtunit.cpp”的末尾,我们看到了EPROM的内容如何加载到RAM中。 对于“ gfxrom”图形资源(与地址0x02000000关联),我们可以看到它们跨越了具有4个地址交替的芯片块中的8 MB的地址空间。 请注意,文件名与芯片的位置相对应(例如,UJ12 / UG12)。 在仿真器世界中,这些EPROM文件的集合被称为“ ROM”。
ROM_START( nbajamte ) ROM_REGION( 0x50000, "adpcm:cpu", 0 ) ROM_LOAD( "l1_nba_jam_tournament_u3_sound_rom.u3", 0x010000, 0x20000, NO_DUMP) ROM_RELOAD( 0x030000, 0x20000 ) ROM_REGION( 0x100000, "adpcm:oki", 0 ) ROM_LOAD( "l1_nba_jam_tournament_u12_sound_rom.u12", 0x000000, 0x80000, NO_DUMP) ROM_LOAD( "l1_nba_jam_tournament_u13_sound_rom.u13", 0x080000, 0x80000, NO_DUMP) ROM_REGION16_LE( 0x100000, "maincpu", 0 ) ROM_LOAD16_BYTE( "l4_nba_jam_tournament_game_rom_uj12.uj12", 0x00000, 0x80000, NO_DUMP) ROM_LOAD16_BYTE( "l4_nba_jam_tournament_game_rom_ug12.ug12", 0x00001, 0x80000, NO_DUMP) ROM_REGION( 0xc00000, "gfxrom", 0 ) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug14.ug14", 0x000000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj14.uj14", 0x000001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug19.ug19", 0x000002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj19.uj19", 0x000003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug16.ug16", 0x200000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj16.uj16", 0x200001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug20.ug20", 0x200002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj20.uj20", 0x200003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug17.ug17", 0x400000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj17.uj17", 0x400001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug22.ug22", 0x400002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj22.uj22", 0x400003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug18.ug18", 0x600000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj18.uj18", 0x600001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug23.ug23", 0x600002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj23.uj23", 0x600003, 0x80000, NO_DUMP) ROM_END
一个有趣的事实:在上面的代码示例中,该函数的最后一个参数被替换为“ NO_DUMP”,以便可以加载修改后的EPROM。 这些字段通常是
[12] EPROM内容的CRC / SHA1哈希。 这就是MAME确定哪个游戏属于ROM的方式,并让您知道游戏集中的ROM之一丢失或损坏。
心脏视频引擎:DMA2
理解图形格式的关键是在256个DMA2寄存器中进行DMA写入/读取的功能,该寄存器位于0x01a80000至0x01a800ff地址。 MAME开发人员已经完成了逆向工程的所有艰苦工作。 他们甚至花时间很好地记录了命令格式。
DMA寄存器
------------------
注册| 位| 申请书
---------- +-FEDCBA9876543210-+ ------------
0 | xxxxxxxx -------- | 每行开头丢弃的像素
| -------- xxxxxxxx | 每行末尾丢弃的像素
1 | x --------------- | 启用记录(或清除为零)
| -421 ------------ | bpp图像(0 = 8)
| ---- 84 ---------- | 在=(1 << x)之后通过
| ------ 21 -------- | 通过大小最大为=(1 << x)
| -------- 8 ------- | 启用之前/之后跳过
| --------- 4 ------ | 启用截断
| ---------- 2 ----- | y镜像
| ----------- 1 ---- | x镜像
| ------------ 8 --- | 传输非零像素作为颜色
| ------------- 4-- | 传输零像素作为颜色
| -------------- 2- | 非零像素传输
| --------------- 1 | 零像素传输
2 | xxxxxxxxxxxxxxxx | 源地址低位字
3 | xxxxxxxxxxxxxxxx | 高字源地址
4 | ------- xxxxxxxxx | x收件人
5 | ------- xxxxxxxxx | 您的收件人
6 | ------ xxxxxxxxxx | 图片栏
7 | ------ xxxxxxxxxx | 图像线
8 | xxxxxxxxxxxxxxxx | 调色板
9 | xxxxxxxxxxxxxxxx | 颜色
10 | --- xxxxxxxxxxxxx | 比例尺
11 | --- xxxxxxxxxxxxx | y标度
12 | ------- xxxxxxxxx | 修剪上/左
13 | ------- xxxxxxxxx | 修整底/右
14 | ---------------- | 测试
15 | xxxxxxxx -------- | 零检测字节
| -------- 8 ------- | 附加页面
| --------- 4 ------ | 收件人大小
| ---------- 2 ----- | 选择寄存器12/13的顶部/底部或左侧/右侧
甚至还有一个调试功能,允许您在将原始sprite传输到DMA2的过程中保存它们(该函数由MAME项目的长期参与者Ryan Holtz
[13]编写)。 我只要玩游戏就足够了,所有带有元数据的文件都保存到磁盘上。
事实证明,子画面由16位调色板的简单元素组成,没有压缩。 但是,并非所有的精灵都有相同数量的颜色。 一些精灵仅使用16种颜色的4位颜色索引,而另一些精灵使用256种颜色并需要8位颜色索引。
贴片
现在,我知道了精灵的位置和格式,因此仍然需要执行最少的逆向工程。 我在Golang上编写了一个小程序,以消除EPROM的“代码”和“ gfx”的交替。 通过消除条带化,可以轻松搜索ASCII或已知值,因为在程序执行过程中我完全按照RAM的外观进行工作。
之后,您可以轻松找到播放器的特征。 原来,它们都是以16位无符号big-endian格式一个接一个地存储的(这很合逻辑,因为34010与big-endian一起工作)。 我添加了一个修补程序来修改播放器属性。 我不太热衷于篮球,所以我输入了SPEED = 9、3 PTS = 9,Dunks = 9,PASS = 9,POWER = 9,STEAL = 9,BLOCK = 9和CLTCH = 9。
我还编写了用新的精灵来打补丁的代码,唯一的限制是-新的精灵的尺寸应与可替换的精灵相同。 对于MJ照片,我创建了256色索引的PNG(您可以
在此处看到)。
最后,我添加了将中间格式转换为交错格式以写入单个EPROM文件的代码。
开始游戏
修补EPROM的内容后,NBAJam诊断工具显示某些芯片的内容标记为“ BAD”。 我之所以这样,是因为我只修补了EPROM的内容,却没有去搜索CRC格式甚至它们的存储位置。
GFX EPROM标记为红色(UG16 / UJ16,UG17 / UJ17,UG18 / UJ18,UG20 / UJ20,UG22 / UJ22和UG23 / UJ23),因为它们包含我更改过的图像。 由于存在调色板,存储指令(UG12和UJ12)的两个EPROM也为红色。
幸运的是,这里的CRC并不用于防止修改内容,而仅用于验证芯片的完整性。 游戏已经开始。 并赚了!
Hasta La Vista,宝贝!
由于技术上的困难,我很快对工具失去了兴趣并停止开发它。 对于那些想玩代码的人的想法:
- 添加到东部会议多伦多猛龙队。
- 添加更改玩家名称的功能。 不幸的是,它们不包含ASCII,而是预生成的图像。
关于NBA Jam的书
如果您是NBA Jam的粉丝,那么瑞安·阿里(Reyan Ali)就她写了一本书
[14] 。 您可以
在这里购买。
源代码
如果您想做出贡献或只是看看一切如何工作,那么完整的源代码将上传到github
here 。
参考文献
[1]来源:
Reyan Ali的“ NJA Jam”[2]来源:
Reyan Ali的“ NJA Jam”[3]来源:
Reyan Ali的“ NJA Jam”[4]来源:
真人快打1幕后花絮[5]来源:
Reyan Ali的“ NJA Jam”[6]资料来源:
4:3对正方形像素[7]评论:不幸的是,如此出色的文档时代已经过去了
[8]来源:
Mame NBA Jam的启动画面[9]来源:
TMS34010指令集[10]资料来源:
T34010用户指南[11]来源:
NBA Jam — BoomShakaLaka视频[12]来源:
MAME T-Unit driver.cpp[13]来源:
Commit'midtunit.cpp:添加了可选的DMA-blitter查看器'[14]来源:
Reyan Ali撰写的“ NBA JAM书”