TI-99/4A:更多依赖固件
TI-99/4A: Leaning More on the Firmware

原始链接: https://bumbershootsoft.wordpress.com/2026/01/17/ti-99-4a-leaning-more-heavily-on-the-firmware/

## TI-99/4A 深度解析:图形与声音重访 去年的TI-99/4A探索揭示了BASIC的局限性,促使我们关注该系统的图形芯片和“图形编程语言”(GPL,为清晰起见,此处称为“GROM代码”)。今年旨在建立在此基础上,特别是增强声音和精灵功能。 TI-99/4A为ROM和GPL字节码(“GROM”)使用不同的内存空间,通过不同的地址范围访问。理解该系统的关键在于十六进制表示法(使用‘>’前缀)和GROM指令命名的特点。 最近的工作集中在利用SN76489声音芯片实现一首巴赫小步舞曲,揭示了其音域的局限性以及由于尺寸限制,声音列表对于复杂音乐的不切实际性。一种潜在的解决方案涉及自定义播放例程,以实现更紧凑的音乐表示。 在精灵动画和碰撞检测方面也取得了进展。利用固件的自动精灵移动系统,成功地对雨伞进行了动画处理,并编程使其在碰撞时反向移动。碰撞检测利用通用的“COINC”指令,需要预先计算的碰撞图。 虽然GROM代码提供了诸如简化程序结构之类的优势,但也存在诸如仅常量索引和限制指针访问之类的局限性。混合ROM/GROM卡带提供了一条潜在的前进道路,并将进行探索。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 TI-99/4A:深入固件 (bumbershootsoft.wordpress.com) 8 分,作者 ibobev 1小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

I kicked off last year with a look at the TI-99/4A home computer. I’d started out playing with BASIC and then moved on to translating some of those BASIC programs into machine language and its custom “Graphics Programming Language” bytecode to see how to approach more serious work. My final few BASIC programs ended in disaster, though, so I skipped those, and of course a handful of BASIC programs weren’t going to hit every facility the system offered either. When surveying the parts I’d missed, I still gave only cursory attention to some of those facilities. Much of the rest of my 2025 revolved around mastering the TI’s graphics chip as it appeared in other systems, and that expertise should let me return and have a better sense of what, exactly, I am looking at if I lean on the firmware’s capabilities.

In particular, I want to look at its enhanced support for sound and sprites. This will mostly be done in the context of the Graphics Programming Language, both because I didn’t do much with that either last year and because many of the features are clearly designed to integrate with it.

We still won’t precisely match the BASIC originals. Happily, that’s because this time, stuff will actually work.

Where We Were

Last year, we learned that TI-99/4A cartridges had two different blocks of memory in them; an ordinary part that mapped into 8KB at location >6000->7FFF (the ROM) and another part, that normally held Graphics Programming Language bytecode, mapped into a completely separate “Graphics ROM” address space from >6000->F7FF (the “GROM”). Which means, now that I return to the TI after an absence, that I’ll also need to reiterate a few of the weird caveats about the system:

  • The tooling around the system uses > as a prefix to represent hexadecimal constants. My usual practice on this blog is to use $ for this, but I follow the system conventions when writing about the TI.
  • “Graphics Programming Language” is usually abbreviated to “GPL” and the code written in it, in both source and bytecoded forms, is “GPL code”; since that means something else these days I will usually call it “GROM code” instead. This isn’t period practice but I think the extra clarity is worth it.
  • Individual GROMs occupy 8KB of address space but are only 6KB in size; the real address space is >6000->77FF, >8000->97FF, and so on. This will figure into some of our design decisions later.
  • GROM instructions don’t seem to have consistent names from source to source. I am leaning on two main sources: the TI-99/4A Tech Pages and the behavior of the xdt99 development toolkit. Since I’m using xdt99 to produce these programs, where these two sources disagree I will use the xdt99 names.

The GROM program that we wrote last year would set up the screen for text display, define some custom graphics, and give us a little banner:

Screenshot: A black box on a green screen, with an umbrella logo and the caption "Bumbershoot Software: 16 more than your TI-83!"

The code for this is pretty brief. (We discussed this code more fully in the second link up top.)

        GROM >6000
        AORG >0000

        DATA >AA01,0,0,MENU
        DATA 0,0,0,0

MENU    DATA 0,START
        STRI "GROM BANNER"

START   ALL >20                         * CLEAR SCREEN
        BACK >12
        DST >0900,@>834A
        CALL >0018                      * SET UPPERCASE CHARSET
        DST >0B00,@>834A
        CALL >004A                      * SET LOWERCASE CHARSET
        MOVE 16,G@COLS,V@>0380          * ASSIGN COLORS
        MOVE 8,G@GFX1,V@>0AF0           * ASSIGN CUSTOM CHARACTERS
        MOVE 32,G@GFX2,V@>0B00
        MOVE 32,G@GFX3,V@>0B40

        FMT
        ROW 10
        COL 0
        HTEXT '  pppppppppppppppppppppppppppp  '
        HTEXT '  p`bppBUMBERSHOOTpSOFTWAREppp  '
        HTEXT '  pak16pMOREpTHANpYOURpTI-83^p  '
        HTEXT '  pppppppppppppppppppppppppppp  '
        FEND

LOOP    SCAN
        BR LOOP
        EXIT

COLS    BYTE >10,>10,>10,>10,>10,>F1,>F1,>F1
        BYTE >F1,>F1,>F1,>F1,>41,>A1,>11,>10

GFX1    BYTE >00,>10,>10,>10,>10,>10,>00,>10
GFX2    BYTE >03,>0F,>1F,>3F,>7F,>7F,>FF,>FF
        BYTE >FF,>FE,>7C,>78,>30,>00,>00,>00
        BYTE >C0,>F0,>F8,>F8,>F0,>E0,>C0,>80
        BYTE >00,>00,>00,>00,>00,>00,>00,>00
GFX3    BYTE >00,>00,>00,>00,>00,>00,>00,>00
        BYTE >00,>00,>00,>00,>00,>00,>00,>00
        BYTE >00,>00,>00,>00,>00,>00,>00,>00
        BYTE >80,>C0,>60,>30,>18,>08,>38,>00

        END START

Meanwhile, we ended our time in the world of BASIC by turning the little umbrella graphic above into sprites and letting them careen around the world. This ended poorly, because we needed two sprites per umbrella, and by the time we got the handle moving, the canopy had already moved a few frames:

Screenshot: The previous two umbrellas, in different locations on the screen because they are moving. The handles started moving at different times, so they appear to be in the wrong places.

The plan for this week is to add those sprites to our code, have them move properly, let them bounce off each other if they collide, and also play a tune in the background as we do.

We’ll start with the music first; it’s more self-contained.

Sound Lists

The core API for the sound lists is deceptively simple: we put the word address of the list at >83CC and write the byte value >01 to >83CE, and the interrupt service routine does the rest. The sound list itself is also very simple: it’s just a series of records, where each record is, in order:

  • A byte specifying the number of bytes to write to the sound chip’s data port.
  • That many bytes of data.
  • The number of frames to wait before processing the next record, or a zero if this is the last entry in the list.

That’s really it, but there are a few gotchas and caveats that we need to be aware of. If you’re doing everything in GROM, everything works out automatically, but if you aren’t…

  • Interrupts must be enabled for anything to happen. If you’re writing machine code, don’t forget your LIMI 2 once you’re done with your frame.
  • The TI-99/4A has three address spaces (CPU, VRAM, and GROM) and we need to tell the system which one we put it in. This is controlled by the least significant bit of >83FD: a 1 bit means the table is in VRAM, and a 0 bit means it’s in GROM. We cannot run sound lists out of the main cartridge ROM. (The default is 0, so, again, an all-GROM approach works out of the box.)
  • The sound processing routine must not be disabled. It won’t be, unless we disabled it ourselves, but the >80 and >20 bits of the byte at >83C2 do have to be zero for any of this to work.

To put the sound system through its paces, I took the Bach minuet I’d used as background music for my SNES and Genesis projects and converted the score to work with the TI’s SN76489 instead. This highlighted a number of additional issues for me. The first was that the base range of this sound chip is frankly not great. The lowest possible tone you can hit is about 110 Hz, which in turn means you only get a few notes below Low C, and which in particular means that I run out of range just before I try to play the G that is the core of the bass line of the Minuet In G. I had to transpose things a bit to get away with this. The second thing we see is that complete songs like this are an awkward fit for the system; it’s clearly imagining itself to be used for sound effects, or little jingles at most. The sound list is total—it is either in use or not and there is no mechanism for mixing together two sources. There’s also no looping mechanism—the best we can do is determine that the sound list system has shut itself down because >83CE is zero again. Furthermore, since sound lists operate at the raw register-write level, lists get very long very fast—if we’re putting any kind of volume envelope on our notes, every step in the volume on every note will each end up being its own record in the sound list. Even with only two voices and nothing more complex than turning notes on and off, this minuet clocked in at nearly 5 kilobytes. GROMs only offer 6KB of contiguous data at a time, so we’re charging towards hardware limits on our very first attempt.

For this song in particular, we do have a simple escape. The minuet has an “AABB” structure; there are really only two parts to the song and they each repeat once. As long as we have to watch for the end of the sound list so that we can loop the song, we might as well have two sound lists, one for the A part and one for the B part, and manage that repetition in the main program logic. That cuts our sound data neatly in half, which results in a much more manageable program.

In the more general case, though, I think the real solution is to abandon sound lists. They just aren’t a great fit for music playback; you’re better off writing your own playroutine that can manage per-note envelopes and which permits more compact representations of notes. That’s what I’ve done in my previous work with the chip and I don’t think the TI’s sound lists can sensibly replace it. That said, I think there is still a place for it if you wish to mix music and sound effects. The sound lists are, after all, just a data stream of register writes. As long as you can ensure that your music routine won’t be interrupted by the sound list processor—and if it’s part of an interrupt handling routine you get that for free—then if the music playback doesn’t touch the same voices as our sound effects then we can use both systems at once. Since we also can trivially know if sound lists are playing, we could even imagine tweaking the music system to share channels with it and leave any preempted channels alone.

That’s overkill for this, though. Here’s my new playback code for managing repetition and looping while still scanning the keyboard for a quit command:

        MOVE 10,G@SONGPAT,V@>1000       * COPY SONG LIST TO VRAM
MUSICLP DST >1000,@>8300                * INITIALIZE SONG POINTER
MUSIC   DCZ V*>8300                     * DOES SONG POINTER POINT TO 0?
        BS MUSICLP                      * IF SO, RESET TO START OF LIST
        DST V*>8300,@>83CC              * OTHERWISE COPY TO SOUND LIST
        DINCT @>8300                    * AND ADVANCE POINTER A WORD
        ST 1,@>83CE                     * START PLAYBACK
LOOP    SCAN                            * KEY HIT?
        BS DONE                         * IF SO, EXIT PROGRAM
        CZ @>83CE                       * IS SOUND LIST STILL PLAYING?
        BR LOOP                         * IF SO, BACK TO KEYBOARD LOOP
        B MUSIC                         * IF NOT, LOAD NEXT SOUND LIST
DONE    EXIT                            * RETURN TO BOOT SCREEN

* MUSIC DATA
SONGPAT DATA MINUETA,MINUETA,MINUETB,MINUETB,0
MINUETA BCOPY "minuet_a.bin"
MINUETB BCOPY "minuet_b.bin"

I think this is the first GROM code I’ve shown that actually attempts to do any indirection. As it turns out, we do not have as free a hand with this as we do in the MOVE commands. We can only index with constants, we can’t access GROM through a pointer, and all pointers have to be in the CPU memory but specifically in the 256-byte scratchpad RAM. We’re not really constrained here, but it’s annoyingly restrictive for what has so far been a much more freewheeling and general-purpose bytecode. I’m particularly annoyed by the fact that I seem to need to copy my table of pointers into VRAM in order to actually iterate through it. I have some ideas for potentially improving this, but they’ll have to wait for next week.

An Alternate Approach: Explicit I/O in the GROM

I’ve described the process above in a way that’s generic enough that you can use the same technique in the GROM or in native code. The bytecode language, however, includes an explicit I/O instruction that lets you command the start of a sound list directly. For my use cases, though, it doesn’t really save you any instructions:

  • Instead of loading the GROM or VRAM address into >83CC, load it into any other place in the scratchpad memory.
  • Instead of starting playback with a write to >83CE, give the command I/O x,addr where x is 0 for a GROM sound list and 1 for a VRAM sound list, and addr is the scratchpad address that holds the actual starting point.

The only place I can see this actually being more concise than just doing it “by hand” is if you’re rapidly shifting between GROM and VRAM for your sound list locations and don’t want to keep rewriting the system flags by hand. But it is there, and the I/O instruction also transparently manages other things like cassette I/O, so it’s nice to see it there.

Automatic Sprite Motion

Now that we have a soundtrack, let’s look at bringing back the animated sprites. We have a little more to deal with here than we did in BASIC, because the VDP’s memory layout is not the same when we boot into GROM. (Indeed, the boot configuration, TI BASIC, TI Extended BASIC, and the Editor/Assembler package are all different, sometimes drastically.) The automatic sprite motion system only puts two constraints on us: the Sprite Attribute Table must be at >0300, (which is to say, VDP Register 5 must hold the value >06), and the sprite motion tables are forced to be in VRAM from >0780->07FF. We’ll need to arrange everything else so that nothing else we care about is in this space.

Records in the sprite motion table are, in order, the Y velocity, the X velocity, and two bytes that the interrupt routine uses as scratch space. Velocities are expressed in units of “pixels per 16 frames”, or, alternately, as pixels per frame represented in a 4.4 fixed-point format.

Implementing Sprite Display

Of course, before we start moving any sprites, we’ll have to define and draw them in the first place. And before we do that, we have to properly configure the VDP. Here are the default values we boot into:

  • Register 0: >00. We are not in bitmap mode.
  • Register 1: >E0. We have all 16KB of VRAM available, we’re in Graphics 1 mode, the display and and display interrupt are both enabled, and sprites are 8×8 unmagnified.
  • Register 2: >00. The Name Table is at >0000.
  • Register 3: >0E. The Color Table is at >0380.
  • Register 4: >01. The Pattern Table is at >0800. (That’s why we load our fonts into >0900; it’s the start of pattern number 32, and thus where the space character should go.)
  • Register 5: >06. The Sprite Attribute Table is at >0300. (That’s just what we want. Good.)
  • Register 6: >00. The Sprite Pattern Table is at >0000. That conflicts with a bunch of our other spaces, including the sprite motion table itself, but as long as we’re only using sprite patterns 128 through 240, no conflicts will actually matter.
  • Register 7: >17. The default colors are black on cyan.

This is mostly fine; we’ll only need to adjust two registers. Register 7 doesn’t have the colors we want, but the BACK >12 instruction ultimately is just a write to that register, so we’ve handled that one already. Register 1 controls, among other things, sprite size, and we want 16×16 magnified sprites. That means the value we want there is >E3. The MOVE command lets us copy data into the VDP registers, which is great for bulk initialization but a little off-base for just writing one. I bounce the value through the CPU scratchpad:

ST >E3,@>83D4
MOVE 1,@>83D4,#1

The screensaver functionality in the firmware needs VDP register 1 to be read-write, and it isn’t; >83D4 is the RAM location where it keeps its own copy of the register value, and normally whenever we write register 1 we need to update the value here first. This code does that, but normally we don’t need to do that in the GROMs; MOVE is smart enough to update the shadow on its own whenever you update this register. Handy, but not for us just now.

The logic from our original BASIC program also doesn’t port directly. BASIC sets up VRAM so that the Sprite Pattern Table overlaps with the main Pattern Table, while the boot environment sets it up so that it instead overlaps with everything else. BASIC is mainly buying the ability to define sprite graphics with CALL CHAR there, but we’d generally prefer the extra graphics space the boot environment gives us. Since we can’t reuse the graphics data we copied into the character pattern table, we’ll need to also copy the umbrella pattern into two places in VRAM during startup. That’s a single line of GPL:

Sprites work the same here as they do anywhere else on the TMS9918A; we copy the attributes for our four sprites into place in the Sprite Attribute Table along with a terminating >D0 byte.

MOVE 17,G@SPRATTR,V@>0300

The sprite data itself has the canvas of the umbrella as sprite pattern 128 and the handle as sprite pattern 132. Placing these is just a table of constants:

SPRATTR BYTE 80,60,>80,4,80,60,>84,10
        BYTE 140,160,>80,7,140,160,>84,14
        BYTE >D0

Now to make them move.

Implementing Sprite Motion

Automatic sprite motion is specified by a 128-byte table in VRAM in >0780->07FF. Like the Sprite Attribute Table, it holds 4 bytes per sprite, and we need to handle every sprite in order from 0 to our highest-index moving sprite. Unlike the Sprite Attribute Table, instead of having a special terminator value, the number of moving sprites is held in the CPU RAM at location >837A. For our four sprites, we load the tables into place and activate them with these two lines of code:

MOVE 16,G@SPRMOVE,V@>0780
ST >04,@>837A

That leaves the table itself. Matching the CALL SPRITE calls from our BASIC code last year is pretty simple:

SPRMOVE BYTE >D8,>14,>00,>00,>D8,>14,>00,>00
        BYTE >0A,>EC,>00,>00,>0A,>EC,>00,>00

Now our umbrellas are flying around properly, and the handles are in the right place:

We’ve reached parity. (Exceeded it, really, since now the code is actually doing what we want, and it didn’t the first time.) Now it’s time to push it further, into parts of the firmware that BASIC didn’t touch.

Sprite Collision Detection

Calling this “sprite collision detection” is a bit of a misnomer. The system does have a sprite collision-detection mechanism—it’s part of the VDP status word—but that only tells us if any sprite is hitting any other sprite. The Graphics Programming Language has a COINC instruction that does far more general collision checking. Instead of relying on pixel-level collisions or trying to sort out which part of a background tile “matters” for the purposes of a hit, it instead takes a generalized collision mask and allows you to check if two objects of those shape overlap if they’re at particular locations.

Unfortunately, it doesn’t do this via some kind of bitmask approach like we saw for the Amiga’s Blitter. Instead the collision information for an X×Y sprite with itself ends up encoded as a (2X+1)×(2Y+1) bit vector, reporting hit or miss for every possible overlap and also all cases where the sprites themselves are merely adjacent (so we may elect to consider a “hit” to include merely touching as well as full overlapping). This representation isn’t always obvious given the shape, so I wrote a small Python script to take our 16×15 sprite and generate the 33×31 collision table. Here’s our umbrella shape, combining the two sprite patterns into a single collision mask:

......XXXX......
....XXXXXXXX....
...XXXXXXXXXX...
..XXXXXXXXXXX...
.XXXXXXXXXXX....
.XXXXXXXXXX.....
XXXXXXXXXX......
XXXXXXXXX.......
XXXXXXXXX.......
XXXXXXX.XX......
.XXXXX...XX.....
.XXXX.....XX....
..XX.......XX...
............X...
..........XXX...

And here’s the resulting collision map, assuming that “touching” counts:

..........XXXXXX.................
........XXXXXXXXXX...............
.......XXXXXXXXXXXXXXXXX.........
......XXXXXXXXXXXXXXXXXXXX.......
.....XXXXXXXXXXXXXXXXXXXXXX......
....XXXXXXXXXXXXXXXXXXXXXXXX.....
....XXXXXXXXXXXXXXXXXXXXXXXXX....
...XXXXXXXXXXXXXXXXXXXXXXXXXX....
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
....XXXXXXXXXXXXXXXXXXXXXXXXX....
....XXXXXXXXXXXXXXXXXXXXXXXXX....
.....XXXXXXXXXXXXXXXXXXXXXXX.....
....XXXXXXXXXXXXXXXXXXXXXXXXX....
....XXXXXXXXXXXXXXXXXXXXXXXXX....
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
...XXXXXXXXXXXXXXXXXXXXXXXXXXX...
....XXXXXXXXXXXXXXXXXXXXXXXXXX...
....XXXXXXXXXXXXXXXXXXXXXXXXX....
.....XXXXXXXXXXXXXXXXXXXXXXXX....
......XXXXXXXXXXXXXXXXXXXXXX.....
.......XXXXXXXXXXXXXXXXXXXX......
.........XXXXXXXXXXXXXXXXX.......
...............XXXXXXXXXX........
.................XXXXXX..........

The basic idea here is that we have two objects (object 1 and object 2), and the upper-left point in this map represents the case where object 2’s upper-left pixel is one pixel down and to the right of object 1’s lower-right pixel. Each other point on the map represents the case where object 1 is displaced that distance to the right or down, with the lower-right pixel representing the case where object 1’s upper-left pixel is just down and to the right of object 2’s lower-right pixel.

This 33×31 collision map then gets copied into 128 bytes like a bitmap, reading 8 bits at a time off the chart, left to right, top to bottom. A slightly wacky thing for us here is that these bits are not aligned; our 33-entry row takes up 4 bytes and then one bit, which ends up in the most significant bit of the next byte with the next row starting immediately afterwards within the same byte. The only padding bits are at the end of the whole map, and it is the least significant bits that are padded.

This table then appended to a four-byte header with size information. The bytes are, in order:

  • The sum of the heights of the two objects.
  • The sum of the widths of the two objects.
  • The height of object 1.
  • The width of object 1.

This was a bit messy to explain, but for further explanation of this you can look at my encoding program or the explanation at the TI-99/4A Tech Pages, which includes some nifty charts working out a full (smaller) example.

There is one issue here that looks like a problem but isn’t; we’re magnifying our sprites here, so our actual object size is not 16×15, but rather 32×30. That would end up being more like half a kilobyte, but the language runtime has our back here; we may specify a “granularity” for our collision table, with 0 being pixel-perfect specification, 1 being expressed in terms of 2×2 squares, 2 using 4×4, and so on. Magnified sprites can use unmagnified collision tables with granularity 1, but it does seem like they imagined this being used for coarser matrices for speed and size savings.

Adding Collision to Our Animation

It’s not enough simply to detect collision, of course; we need to do something about it, too. I didn’t want to get too fancy here so I decided to make the umbrellas reverse their horizontal velocity if they hit. This is nothing at all like a real physics simulation, so most likely this motion change won’t actually make them stop colliding. I don’t want them to end up “wedged”, so I’ll want to set a timer so that collisions aren’t possible for a few dozen frames after a hit.

These collision checks need to be done while we’re also managing scanning the keyboard and monitoring the music playback, but they’re also intricate enough that I want to break them out into their own function. This is much easier to do in the GROM than in native code; one reason our scratchpad space was so desperately scarce in our native code is that the GPL interpreter reserves 25% of it for its own stacks. This means that its CALL and RTN instructions will let us design our programs in a far more traditional way as long as we don’t recurse too deep. I put a CALL FRAME instruction just before the keyboard scan in that main loop, and then put the rest in that function.

(In my first visit to the system, I ended up designing a rather Byzantine ABI that kept most program state in VRAM and used most of what little scratchpad RAM remained to us as a local cache of it. I’m overdue to revisit that design; I’ll probably take a fresh look at it once I wrap this series up.)

The first thing the FRAME function has to do is actually restrict itself to running once per frame; we’d just been spinning as fast as we could, before, because we didn’t have anything timing-related that we had to care about. The firmware just did everything for us. That’s no longer true thanks to the collision timer, so our first task is to spin on the frame counter in >8379 until it changes. That will be our proof that the interrupt service routine ran:

FRAME   ST @>8379,@>8302
FWAIT   CEQ @>8379,@>8302
        BS FWAIT

Now we need to check our collision timer, which I’ve put in >8303. If it’s zero, we move on with our real check, but otherwise we just decrement that counter and return immediately.

        CZ @>8303
        BS FCHECK
        DEC @>8303
        RTN
FCHECK

The check itself is kind of wacky. We have to pass the two locations we’re checking as arguments, which is normal enough. Each argument is a 16-bit value that holds a byte for the Y coordinate of that object and then a byte for the X coordinate. This matches neatly to the values stored in the Sprite Attribute Table, so we can simply use the entries for our two main umbrella sprites here.

The strangeness comes for the rest of the instruction; after writing a COINC instruction, we need to place a byte value (the granularity) and a word value (the address of the collision table) directly into the instruction stream as raw data. This is a general feature of GROM code; there’s a FETCH instruction that pulls bytes out of the caller’s instruction stream and advances the return address appropriately. It’s a little weird, though, because this is all ROM code and these values can only be hardcoded constants. They’re not really usable as part of a loop, or based on program state, or anything at all like that.

FCHECK  COINC V@>0300,V@>0308
        BYTE >01
        DATA COLLIDE

The COINC instruction sets the condition bit if there’s a collision. We branch right to the return statement if there isn’t one, and otherwise, we set our collision timer to 128 frames and then negate all our sprites’ X velocities in the automatic motion table.

We’re in danger here, though; each umbrella is made of two sprites, and if we have a fresh interrupt mid-edit the handle and canopy could change their directions at different times and start drifting apart. That’s what mangled the umbrellas in our original BASIC program, so we’d better make sure we don’t recreate the same disaster.

The solution is to zero out the “moving sprite” count at >837A before our edits and restore it to 4 afterwards. That way, if we get interrupted partway through the frame the sprites will just stop instead of diverge.

        BR FDONE
FSTRIKE ST @>80,@>8303
        CLR @>837A
        NEG V@>0781
        NEG V@>0785
        NEG V@>0789
        NEG V@>078D
        ST >04,@>837A

FDONE   RTN

Moving Onward

This was interesting; we got quite a bit more use out of the Graphics Programming Language this week, and the capabilities the firmware offers here are usefully general and sometimes (for the sprite coincidence checks) only available via the Graphics Programming Language. Pure machine-code programs would need to do quite a bit of work to match COINC, but it’s not unreasonable that they might want to use the sound list and sprite motion features on their own sometimes too. There are plenty of extra caveats, but none of them are dealbreakers.

I did struggle a bit against the GROM code itself, though. There are some very handy affordances here, but there are also weird or wonky limitations, and I don’t think I’d generally be thrilled with the idea of writing something purely in GROM.

Fortunately, we don’t have to; there are robust facilities for making hybrid ROM/GROM cartridges, and next week I’ll look at that and where we’d want to do them.

联系我们 contact @ memedata.com