我看过很酷的 C64 演示,在屏幕的边界区域显示精灵。这不应该是可能的;我认为他们设法以某种方式欺骗了图形芯片。他们究竟是怎么做到的?
6 回答
是的,你需要汇编程序。这是一个中断计时技巧。VIC 能够在边框中显示精灵,但框架只是将它们隐藏起来,因此精灵可以在其后面滑动。它连接到 VIC 显示的扫描线。对于下/上边界,它非常简单:
- 编程中断,同步以在下边界之前的某个扫描线、7 像素或类似的地方开始。
- 设置VIC中的寄存器使边框变小。(有一个寄存器可以做到这一点。)
- VIC 现在认为边界已经开始并且没有开始绘制它。
- -> 底部没有边框。
- 在真实边界之后编程另一个中断以将其设置回原始边界。
对于左/右边界的精灵,它更复杂,因为必须对每条扫描线重复该过程:
- 编程一个中断,同步在某个扫描线开始。
- 然后做一些 NOP,直到你在右边框前 7 个像素处。
- 设置VIC中的寄存器使边框变小。
- -> 右侧没有边框。
- 做一些 NOP,直到你在真正的边界之后并将寄存器设置回原始值。
- 再次执行一些 NOP,直到第 2 步。
问题是所有这些 NOP 都在忙着等待,并窃取了你为你的东西所拥有的周期。
我能够从下边框的精灵滚动条中为您找到一些代码。这是代码。(它是从一些演示中撕下来的。)
C198 78 SEI
C199 20 2E C1 JSR C12E # clear sprite area
C19C 20 48 C1 JSR C148 # init VIC
C19F A9 BF LDA #BF # set up IRQ in C1BF
C1A1 A2 C1 LDX #C1
C1A3 8D 14 03 STA 0314
C1A6 8E 15 03 STX 0315
C1A9 A9 1B LDA #1B
C1AB 8D 11 D0 STA D011
C1AE A9 F7 LDA #F7
C1B0 8D 12 D0 STA D012
C1B3 A9 01 LDA #01
C1B5 8D 1A D0 STA D01A
C1B8 A9 7F LDA #7F
C1BA 8D 0D DC STA DC0D
C1BD 58 CLI
C1BE 60 RTS
----------------------------------
# init VIC
C148 A2 00 LDX #00
C14A BD 88 C1 LDA C188,X
C14D 9D 00 D0 STA D000,X # set first 16 values from table
C150 E8 INX
C151 E0 10 CPX #10
C153 D0 F5 BNE C14A
C155 A9 FF LDA #FF
C157 8D 15 D0 STA D015
C15A A9 00 LDA #00
C15C 8D 1C D0 STA D01C
C15F A9 FF LDA #FF
C161 8D 17 D0 STA D017
C164 8D 1D D0 STA D01D
C167 A9 C0 LDA #C0
C169 8D 10 D0 STA D010
C16C A9 F8 LDA #F8
C16E A2 00 LDX #00
C170 9D F8 07 STA 07F8,X
C173 18 CLC
C174 69 01 ADC #01
C176 E8 INX
C177 E0 08 CPX #08
C179 D0 F5 BNE C170
C17B A9 0E LDA #0E
C17D A2 00 LDX #00
C17F 9D 27 D0 STA D027,X
C182 E8 INX
C183 E0 08 CPX #08
C185 D0 F8 BNE C17F
C187 60 RTS
----------------------------------
# data set into VIC registers
C188 00 F7 30 F7 60 F7 90 F7
C190 C0 F7 F0 F7 20 F7 50 F7
----------------------------------
# main IRQ routine
C1BF A2 08 LDX #08
C1C1 CA DEX
C1C2 D0 FD BNE C1C1
C1C4 A2 28 LDX #28 # 40 or so lines
C1C6 EA NOP # "timing"
C1C7 EA NOP
C1C8 EA NOP
C1C9 EA NOP
C1CA CE 16 D0 DEC D016 # fiddle register
C1CD EE 16 D0 INC D016
C1D0 AC 12 D0 LDY D012
C1D3 88 DEY
C1D4 EA NOP
C1D5 98 TYA
C1D6 29 07 AND #07
C1D8 09 18 ORA #18
C1DA 8D 11 D0 STA D011
C1DD 24 EA BIT EA
C1DF EA NOP
C1E0 EA NOP
C1E1 CA DEX
C1E2 10 E4 BPL C1C8 # repeat next line
C1E4 A9 1B LDA #1B
C1E6 8D 11 D0 STA D011
C1E9 A9 01 LDA #01
C1EB 8D 19 D0 STA D019
C1EE 20 00 C0 JSR C000 # call main code
C1F1 4C 31 EA JMP EA31 # finish IRQ
这一切都取决于时机。C64 有一种方法可以在绘制屏幕时查询电子束的确切垂直位置。当新行开始时,您必须等待几个周期(您可以使用 NOP 指令计时),然后您必须设置负责设置屏幕模式(和边框宽度)的视频芯片的硬件寄存器。通过完全正确地计时,并在每条扫描线上再次进行,整个边线消失了。
底部边框采用了类似的技巧。在垂直边框开始的确切扫描线上,您也必须设置禁用该帧底部边框的视频模式。
事实上,这整件事必须在组装中完成。否则,您永远无法准确把握时机。
作为旁注,我认为边际技巧归功于 1001 Crew(一个荷兰团体)。我不确定是谁完成了第一个底部边框技巧。
有关在 C64 上打开边界主题的优秀教程,请查看 Pasi Ojala 在C=Hacking 第 6 期中的优秀文章。
无需过多技术,该技巧使用 VIC 芯片的功能让您在 25/24 行和 40/38 列的文本/图形之间切换,并涉及在正确的时间进行此切换以欺骗 VIC 思考它已经打开了边界,而实际上并没有。查看上面的文章以获得更全面的代码示例解释。
那是很久以前的事了。
我知道有一个依赖于监视器频率的解决方案。
使用 CRT,即使当前像素在正常屏幕之外,它也是已知的。所以你可以操纵射线。
在我的垃圾堆里一定有一些 C64 书籍。
离题,但 VIC20(C64 的前身)的图形很有趣。无法操纵每个像素,但您可以更改现有字符。因此,您用从 0 到 ... 的所有字符填充了屏幕,并更改了字符以将像素设置为屏幕。;-)。
正如已经说过的,你必须愚弄 VIC 认为边界已经开始,但我之所以写这个是因为上面的答案有点不精确:我完全找不到一个寄存器来制作边界更小,所以这就是你这样做的方式(至少对于顶部和底部):你等到 VIC 到达第 25 个字符行,然后你启用 24 行($D011,位 3)。你可以对左右边框做同样的事情,只用 38 列($D016,位 3),但要做到这一点,你需要非常精确的时间,你还需要通过设置垂直滚动寄存器来消除坏线,所以扫描线 mod 8 永远不会等于滚动值。当然,你不能再使用正常显示了,因为坏行实际上不仅仅是坏的,我认为它们是用来加载字符数据的,在非边界区域每 8 行重复的东西。当我阅读最佳答案时,我个人有点困惑,希望能有所帮助。(另外,上面的答案有一个错误:你没有让边框变小,你让它变大了)
时机是关键。当 CRT 的光束从左向右移动时,通过改变过扫描(边框)颜色在边框中创建了图像。产生图像需要两个时序信号——垂直刷新和水平刷新。通过检测水平和垂直刷新何时发生,您可以启动一系列汇编指令来更改边框颜色以生成图像。您需要计算出每个边框像素的 CPU 时钟滴答数,并使用它来创建在正确点更改边框颜色的代码。
它在编写游戏时效果不佳,因为 CPU 开销太大,无法处理用户输入和游戏状态。