Monday, December 21, 2015

Lord Victorino and Bard demo Dragon's Lair 2 and Dexter

Wednesday, December 9, 2015

Dexter pre-order period planned to begin on January 1st of 2016

Based on my recent survey and with discussions with multiple potential Dexter customers, I have concluded that at least 50% of people want the Dexter pre-order period to be in 2016 rather than at the end of 2015.  Therefore, my plan is to start accepting pre-orders for the first non-prototype version of Dexter on January 1st of 2016.   I will be posting more about this at a future time.

Sunday, November 29, 2015

Dexter pre-order poll

When should I take pre-orders for the final version of Dexter?  December is a busy time of year, so I am leaning toward January, but maybe December would be better.  What do you think?

Vote here .

Results here .

Tuesday, November 24, 2015

I've been having email problems.

I've had email problems for at least a month with the email account linked to my Dexter "let me know when it's ready" link on the Dexter web page.  So if you've tried to send me an email that way, I probably did not receive it.  I'm very sorry for the inconvenience.  It should be fixed now!

Thursday, November 19, 2015

Star Rider clever FIRQ handler revealed

In furtherance of my goal to get Star Rider working with Dexter, I decoded the FIRQ handler in the Star Rider main CPU's ROM.  FIRQ stands for Fast IRQ and is an alternate interrupt available on the 6809E CPU which is "fast" because it does not push all of the registers on the stack before executing (meaning the programmer will have to assume this responsibility instead but has the option to use the registers cleverly to squeeze out some extra performance).

The Star Rider FIRQ handler is pretty nifty because it is designed to be called when the hardware blitter "special" chips have finished an operation and immediately give the blitter "special" chips more work to do.  The neat thing about this design is that the main CPU can still be doing other work while the blitter chips are running.  In previous Williams' games, such as Joust, the blitter chip halts the main CPU and no work can be done while the blitter chip is doing its thing.  With Star Rider, the hardware guys (apparently) realized that halting the main CPU was a potential waste of time and instead let the main CPU keep running while simultaneously disabling the main CPU's access to the video/DMA bus.  It's a really clever design for 1983/1984 or whenever.

Also cool is that the FIRQ handler can be called without an interrupt occurring (see $EAD9).  If called in this way, the program will mimic what the CPU does for a real FIRQ so that the RTI (return from interrupt) instruction works correctly regardless of whether there was a real FIRQ or if the program faked it.

Finally, the FIRQ handler is even more nifty because it has an optional "wait for vsync" type behavior built-in.  It can optionally check to see whether the next blit will cause visible vertical tearing and if so delay the blit until this is no longer a concern.  I am not sure if this code actually ends up getting used in production because, just from eyeballing it, I am not 100% convinced that the vertical counter hardware value matches up with the actual vertical line value (maybe it does!) and the ROM program definitely seems to be making this assumption.  Decide for yourself :)

ROM:EAD9 ; =============== S U B R O U T I N E =======================================
ROM:EAD9 ; Execute the FIRQ interrupt handler without the FIRQ interrupt firing.
ROM:EAD9 ; This appears to create an environment where RTI will act like RTS.
ROM:EAD9 ; NOTE: it appears that IRQ's are not disabled
ROM:EAD9 FakeFIRQ:                               ; CODE XREF: DoBlit:loc_0_EA90 P
ROM:EAD9                                         ; sub_0_EAB4+18 P
ROM:EAD9                 tfr     cc, a
ROM:EADB                 anda    #$7F ; ''      ; disable E flag (for subsequent RTI call)
ROM:EADD                 pshs    a               ; save CC flags (For subsequent RTI call)
ROM:EADF                 orcc    #%1000000       ; disable F flag (for subsequent RTI call)
ROM:EAE1 FIRQ:                                   ; DATA XREF: ROM:FFF6 o
ROM:EAE1                 tst     PIA_VGG_U7_PERFDIR_A ; clear PIA's IRQA'
ROM:EAE4                 dec     BlitterPendingOperationsCount ; When FIRQ gets called, it means a blitter operation just finished.
ROM:EAE4                                         ; So decrement the pending count.
ROM:EAE7                 beq     FIRQ_End        ; branch if we have no more blitter operations left
ROM:EAE9                 pshs    a,b,x           ; save regs (since this is FIRQ, they won't be automatically saved)
ROM:EAEB                 ldx     BlitterContextPtr ; points to the current blitter context to be sent to the blitter hardware
ROM:EAEE                 lda     BlitDontAvoidTearing ; 0: try to avoid vsync tearing artifacts
ROM:EAEE                                         ; 1: blit as fast as possible ignoring vertical tearing
ROM:EAEE                                         ; (see EAEE)
ROM:EAF1                 bne     DoBlitAndAdvanceBlitterContextPtr ; X points to the blitter context
ROM:EAF3                 lda     8,x
ROM:EAF5                 bmi     DoBlitAndAdvanceBlitterContextPtr ; branch if IMPG value has high-bit set
ROM:EAF7                 lda     5,x             ; A = blitter destination Y value
ROM:EAF9                 adda    7,x             ; add blit size height to A
ROM:EAF9                                         ; This makes A now the bottom coordinate of the blit rectangle
ROM:EAFB                 suba    HW_VerticalCounter ; Subtract current vertical counter value from blit bottom Y value.
ROM:EAFB                                         ; If positive, beam is above bottom of blit can tearing can occur.
ROM:EAFB                                         ; If negative, beam is below bottom of blit and tearing is not a risk.
ROM:EAFB                                         ; AT LEAST that's what it is looking like to me! :)
ROM:EAFE                 bls     DoBlitAndAdvanceBlitterContextPtr ; branch if tearing is not a concern
ROM:EB00                 ldb     #65             ; this appears to create a new blit size
ROM:EB00                                         ; of up to 64x65 pixels, but can be less.
ROM:EB00                                         ; It appears to be a way to stall to avoid tearing artifacts on the screen.
ROM:EB02                 cmpa    #64
ROM:EB04                 bcs     DoNullBlit      ; if width is already less than 64 pixels, branch
ROM:EB06                 lda     #64             ; force width down to 64 pixels
ROM:EB08 DoNullBlit:                             ; CODE XREF: FakeFIRQ+2B j
ROM:EB08                 std     HW_BlitterSize
ROM:EB0B                 inc     BlitterPendingOperationsCount ; make sure next time FIRQ fires, the blit we didn't perform gets redone
ROM:EB0E                 lda     #%11000000      ; this appears to execute a NOP to the blitter chips by suppressing all writes.  It may be to queue up a future FIRQ.
ROM:EB10                 sta     HW_BlitterControl ; writing here executes the blitter chips.
ROM:EB10                                         ; Control Bits
ROM:EB10                                         ;  7: 1=write suppress odd pixels
ROM:EB10                                         ;  6: 1=write suppress even pixels
ROM:EB10                                         ;  5: 1=shift image 1 pixel to the right (odd flavor)
ROM:EB10                                         ;  4: 1=solid color mode (constant substitution)
ROM:EB10                                         ;  3: 1=zero write suppress (only blit non-zero color values)
ROM:EB10                                         ;  2: 1=sync to E clock (half speed writes, RAM to RAM)
ROM:EB10                                         ;  1: write format (0=serial, 1=block)
ROM:EB10                                         ;  0: read format (0=serial, 1=block)
ROM:EB13                 bra     FIRQ_RestoreRegsThenEnd
ROM:EB15 ; ---------------------------------------------------------------------------
ROM:EB15 DoBlitAndAdvanceBlitterContextPtr:      ; CODE XREF: FakeFIRQ+18 j
ROM:EB15                                         ; FakeFIRQ+1C j ...
ROM:EB15                 ldd     8,x             ; X points to the blitter context
ROM:EB17                 std     HW_IMPG_PRIME
ROM:EB1A                 ldd     6,x
ROM:EB1C                 std     HW_BlitterSize
ROM:EB1F                 ldd     4,x
ROM:EB21                 std     HW_BlitterDest
ROM:EB24                 ldd     2,x
ROM:EB26                 std     HW_BlitterSource
ROM:EB29                 ldd     ,x
ROM:EB2B                 stb     HW_BlitterSolidClr
ROM:EB2E                 sta     HW_BlitterControl ; writing here executes the blitter chips.
ROM:EB2E                                         ; Control Bits
ROM:EB2E                                         ;  7: 1=write suppress odd pixels
ROM:EB2E                                         ;  6: 1=write suppress even pixels
ROM:EB2E                                         ;  5: 1=shift image 1 pixel to the right (odd flavor)
ROM:EB2E                                         ;  4: 1=solid color mode (constant substitution)
ROM:EB2E                                         ;  3: 1=zero write suppress (only blit non-zero color values)
ROM:EB2E                                         ;  2: 1=sync to E clock (half speed writes, RAM to RAM)
ROM:EB2E                                         ;  1: write format (0=serial, 1=block)
ROM:EB2E                                         ;  0: read format (0=serial, 1=block)
ROM:EB31                 leax    10,x            ; advance to the next blitter context in the array
ROM:EB31                                         ; (each context is 10 bytes long)
ROM:EB33                 cmpx    #word_0_AC1C    ; have we got to the end of the blitter contexts?
ROM:EB36                 bcs     StoreNewBlitterContextPointer
ROM:EB38                 ldx     #BlitterContextArray ; a 1000 byte array which has room for 100 10-byte blitter context entries.
ROM:EB3B StoreNewBlitterContextPointer:          ; CODE XREF: FakeFIRQ+5D j
ROM:EB3B                 stx     BlitterContextPtr ; points to the current blitter context to be sent to the blitter hardware
ROM:EB3E FIRQ_RestoreRegsThenEnd:                ; CODE XREF: FakeFIRQ+3A j
ROM:EB3E                 puls    x,b,a
ROM:EB40 FIRQ_End:                               ; CODE XREF: FakeFIRQ+E j
ROM:EB40                 rti
ROM:EB40 ; End of function FakeFIRQ

Monday, November 16, 2015

Dexter Star Rider search bug solution

My birthday party is over (don't think that I mentioned that on here) so I now am turning my focus back to shipping Dexter.

Star Rider has almost worked perfectly with Dexter for quite some time and I had the opportunity to do more Star Rider research recently and here's some of my interesting findings.

First of all, the reason that Dexter wasn't working was because during the self-test, it would routinely fail with a search to frame 6 (as I recall) but would usually be able to successfully seek to other test frames on the disc.  Why was it always failing to seek to frame 6?

Well, after much studying of the ROM disassembly, I think I have a pretty good grasp of how it works.

First of all, to initiate a seek, the Star Rider program calls a function at $7E75:

ROM3:7E75 ; =============== S U B R O U T I N E =======================================
ROM3:7E75 ; frame number stored in D
ROM3:7E75 ; on return carry may be set if advance button was pressed
ROM3:7E75 InitiateDiscSearch:                     ; CODE XREF: DiscTestSearchTo+2F P
ROM3:7E75                                         ; ROM3:7DE7 P ...
ROM3:7E75                 pshs    x,y,u
ROM3:7E77                 pshs    a,b
ROM3:7E79                 clra
ROM3:7E7A                 clrb
ROM3:7E7B                 std     PifSendError    ; Will contain an error code (such as 0x507) if trying to send commands to the PIF board or read laserdisc VBI data failed.
ROM3:7E7B                                         ; (see 19e2).  Otherwise it will be 0.
ROM3:7E7E                 lda     #$17
ROM3:7E80                 sta     PifResponseRetryCount
ROM3:7E83                 puls    b,a
ROM3:7E85                 jsr     SetupPifBuffersForSearching ; It appears that this is a "search to frame" function, where the frame is stored in D
ROM3:7E85                                         ; (A1BC needs to get a non-zero value stored to it or else it's an error)
ROM3:7E85                                         ; A1A4: 0x5 0x5 0x13 FrameHi FrameLo 0x5
ROM3:7E85                                         ; A1B4: FrameHi FrameLo
ROM3:7E85                                         ; A1BC: 0x13 0x0A 0x64
ROM3:7E85                                         ;
ROM3:7E85                                         ; A1A7 and A1B4 = D
ROM3:7E85                                         ; A1A4 = 0x505
ROM3:7E85                                         ; A1A6,A1BC = 0x13
ROM3:7E85                                         ; A1BD = 0xA64
ROM3:7E85                                         ; A1A9 = 5
ROM3:7E88 WaitSearchFinish:                       ; CODE XREF: InitiateDiscSearch+21 j
ROM3:7E88                 clra
ROM3:7E89                 ldd     PifSendError    ; Will contain an error code (such as 0x507) if trying to send commands to the PIF board or read laserdisc VBI data failed.
ROM3:7E89                                         ; (see 19e2).  Otherwise it will be 0.
ROM3:7E8C                 bne     Done
ROM3:7E8E                 jsr     IfAdvancePressedSetCarry ; set carry if advance is pressed
ROM3:7E8E                                         ; clear carry if advance not pressed
ROM3:7E91                 bcs     Done
ROM3:7E93                 jsr     TestExpectedFrameNumStatus ; Does a TST on memory location A1BC
ROM3:7E93                                         ; Z will be set if expected frame number matches actual frame number.
ROM3:7E96                 bne     WaitSearchFinish
ROM3:7E98                 ldd     PifSendError    ; Will contain an error code (such as 0x507) if trying to send commands to the PIF board or read laserdisc VBI data failed.
ROM3:7E98                                         ; (see 19e2).  Otherwise it will be 0.
ROM3:7E9B Done:                                   ; CODE XREF: InitiateDiscSearch+17 j
ROM3:7E9B                                         ; InitiateDiscSearch+1C j
ROM3:7E9B                 puls    pc,u,y,x
ROM3:7E9B ; End of function InitiateDiscSearch

The summary is that this method calls another function at $7E85 to store data in some buffers that will be later sent to the PIF board and then it waits for either the search to succeed ($7E98) or for the search to fail with an error ($7E8C).

Here is the function at $7E85:

ROM1:1C5B ; =============== S U B R O U T I N E =======================================
ROM1:1C5B ; It appears that this is a "search to frame" function, where the frame is stored in D
ROM1:1C5B ; (A1BC needs to get a non-zero value stored to it or else it's an error)
ROM1:1C5B ; A1A4: 0x5 0x5 0x13 FrameHi FrameLo 0x5
ROM1:1C5B ; A1B4: FrameHi FrameLo
ROM1:1C5B ; A1BC: 0x13 0x0A 0x64
ROM1:1C5B ;
ROM1:1C5B ; A1A7 and A1B4 = D
ROM1:1C5B ; A1A4 = 0x505
ROM1:1C5B ; A1A6,A1BC = 0x13
ROM1:1C5B ; A1BD = 0xA64
ROM1:1C5B ; A1A9 = 5
ROM1:1C5B SetupPifBuffersForSearching:            ; CODE XREF: sub_0_35A0+C P
ROM1:1C5B                                         ; sub_0_4471+98 P ...
ROM1:1C5B                 pshs    cc
ROM1:1C5D                 orcc    #$F0 ; '='      ; suppress interrupts
ROM1:1C5F                 std     PifArray5Bytes_3
ROM1:1C62                 std     ExpectedHexPicNum ; The picture number we expect to be on (used to detect successful searches, etc).
ROM1:1C62                                         ; (see 1b9c)
ROM1:1C65                 ldd     #$505           ; 5 is a NOP command, so this starts with two NOP commands
ROM1:1C68                 std     PifArray5Bytes  ; this array will be sent to pif board if the first byt is not zero
ROM1:1C68                                         ; it is 5 bytes in size
ROM1:1C6B                 lda     #$13            ; 13 is the FIRQ cmd to initiate disc search
ROM1:1C6D                 sta     PifArray5Bytes_2
ROM1:1C70                 sta     ExpectedFrameNumStatus ; May indicate current/pending pif command. (see 1a5f)
ROM1:1C70                                         ; gets 0x13 stored when a new search is initiated (FIRQ search command)
ROM1:1C73                 ldd     #$A64           ; 0xA is a slow decrementing counter that determines when we start calling the subroutine at 1b68,
ROM1:1C73                                         ; while 0x64 is an even slower decrementing counter that, if it reaches 0, triggers a 507 error (see 1b7f)
ROM1:1C76                 stb     SlowerPifCounter
ROM1:1C79                 sta     SlowPifCounter  ; may throttle how often we check for seek complete among other things
ROM1:1C7C loc_0_1C7C:                             ; DATA XREF: sub_0_2DDA+19A r
ROM1:1C7C                 jsr     Store5ToA1A9    ; A1A9 = 5
ROM1:1C7F                 puls    pc,cc
ROM1:1C7F ; End of function SetupPifBuffersForSearching

This function was quite difficult to decode.  It basically stores some commands to be sent to the PIF board at a later time in some buffers.  Of particular note is a 5-byte array which will look like this: 0x05 0x05 0x13 FrameHi FrameLo.  By disassembling the PIF board ROM, I was able to discover that command 0x5 is a single byte NO-OP while command 0x13 is a 'search to frame' command which is followed by the desired frame number in binary/hex format.

So when does this command actually get executed?

It happens during the IRQ service routine, which gets triggered on vblank.

We'll skip some details and start at $1A5F which gets called every top field and checks to see if there's anything to send to the PIF board.  In our case, after the search command has been received, we will end up here:

ROM1:1A97 A1BCNotZeroAmongOtherTHings:            ; CODE XREF: SendPifCmdsAndMore+5 j
ROM1:1A97                                         ; SendPifCmdsAndMore+A j ...
ROM1:1A97                 orcc    #$F0 ; '='      ; disable interrupts
ROM1:1A99                 lda     PIA_VGG_U7_CTRL_B ; PIF PIA
ROM1:1A9C                 lbpl    DecreaseCounterAndCheck ; branch if pif board is not ready / waiting for seek to finish (?)
ROM1:1AA0                 ldx     NextPifBufStruct ; seems to contain a pointer to the next pif buffer structure after pif board becomes ready again (see 1AA0)
ROM1:1AA3                 beq     DecrementPriorityCounter ; branch if there is no active policy
ROM1:1AA5                 ldb     PifTimeoutBytesLeftToSend ; do we need to finish sending pif bytes we tried to send previously?
ROM1:1AA8                 beq     NoPifBytesLeftToSend ; if we have no bytes left to send from a previous attempt, branch
ROM1:1AAA                 ldu     #PifTimeoutSendBuf ; If sending to pif timed out, this buffer
ROM1:1AAA                                         ; will hold what we still need to send after
ROM1:1AAA                                         ; the pif board becomes ready again.
ROM1:1AAA                                         ; The array size is 4 bytes because the max
ROM1:1AAA                                         ; size of a pif send buffer is 5, so at
ROM1:1AAA                                         ; most 4 bytes will not have been sent if
ROM1:1AAA                                         ; the pif board becomes busy.
ROM1:1AAD                 jsr     SendCommandsToPif ; B holds how many bytes to send and will be modified
ROM1:1AAD                                         ; U points to array of bytes to send and will be modified
ROM1:1AAD                                         ; carry is set if we don't get the expected response
ROM1:1AB0                 bcs     OnPifTimeout    ; if we timed out, it can mean that a seek has initiated (maybe can mean other things)
ROM1:1AB0                                         ; and so we should wait for the pif board to become ready
ROM1:1AB0                                         ; before sending anything else (within reason timeout period).

$1A99 checks to see if the PIF board is 'ready' or not.  If it returns a result with the high-bit clear, it means the PIF board is not ready, so the program branches to the end of this function and just decrements a timer.  If this timer expires, the PIF board is assumed to have become unresponsive and a fatal error is thrown.  It is pretty important to understand that as long as the PIF board is 'busy', this method won't try to check to see whether the laserdisc is on the proper frame.

So once the PIF board has become non-busy, the program will then try to match up the frame number.

ROM1:1B68 ; =============== S U B R O U T I N E =======================================
ROM1:1B68 ; If enough time has passed, check to see if the expected picture number equals the actual picture number.
ROM1:1B68 ; If there is a discrepancy, send PIF jump commands to compensate.
ROM1:1B68 ; If enough time hasn't passed, this will update the timer/counter.
ROM1:1B68 ;
ROM1:1B68 ; This sets Z flag on success (D = 0) and return an error code in D on error.
ROM1:1B68 ThinkActualFrameNumIsExpected:          ; CODE XREF: SendPifCmdsAndMore+5E P
ROM1:1B68                                         ; SendPifCmdsAndMore+98 P
ROM1:1B68                 lda     SlowPifCounter  ; may throttle how often we check for seek complete among other things
ROM1:1B6B                 beq     CompareActualPicNumWithExpected
ROM1:1B6D                 dec     SlowPifCounter  ; may throttle how often we check for seek complete among other things
ROM1:1B70                 beq     ReadActualPicNumIfAvailable
ROM1:1B72                 jsr     Store5ToA1A9    ; make sure that this gets called again
ROM1:1B75                 clra                    ; sets Z flag to indicate success
ROM1:1B76                 rts
ROM1:1B77 ; ---------------------------------------------------------------------------
ROM1:1B77 ReadActualPicNumIfAvailable:            ; CODE XREF: ThinkActualFrameNumIsExpected+8 j
ROM1:1B77                 lda     #4
ROM1:1B79                 sta     SlowPifCounter  ; may throttle how often we check for seek complete among other things
ROM1:1B7C                 dec     SlowerPifCounter
ROM1:1B7F                 beq     Error507        ; if this counter expires, it's an error
ROM1:1B81                 lda     FieldsLeftBeforeStability ; If this is 0, it means it's safe to read the VBI picture number. (See 1B81)
ROM1:1B81                                         ; Gets decremented by IRQ if it is not 0.
ROM1:1B81                                         ; It indicates how many fields the IRQ needs to see before it considers the software/hardware field values to be stable. (see e488)
ROM1:1B81                                         ; It gets set to non-zero any time the hardware field value does not match the software field value.
ROM1:1B84                 bne     Skip4TracksForward ; skip 4 tracks forward
ROM1:1B84                                         ; (to recover from bad VBI read?)
ROM1:1B86                 jsr     ConvertBCDPicNumToHexPicNum ; Converts the last read BCD picture number (from laserdisc VBI) to a hex picture number.
ROM1:1B86                                         ; Carry will be clear on success or set on failure.
ROM1:1B86                                         ; 'D' and $A133 will contain the converted picture number, or 50000 (decimal) on failure.
ROM1:1B89                 bcc     CompareActualPicNumWithExpected
ROM1:1B8B Skip4TracksForward:                     ; CODE XREF: ThinkActualFrameNumIsExpected+1C j
ROM1:1B8B                 lda     #$10            ; skip 4 tracks forward
ROM1:1B8B                                         ; (to recover from bad VBI read?)
ROM1:1B8D                 sta     PifArray1ByteSpecial ; This byte will be sent to the pif board if it is non-zero.
ROM1:1B8D                                         ; It is special in that it can receive a higher priority to be sent (I don't fully understand it yet).
ROM1:1B90                 clra
ROM1:1B91                 rts
ROM1:1B92 ; ---------------------------------------------------------------------------
ROM1:1B92 Error507:                               ; CODE XREF: ThinkActualFrameNumIsExpected+17 j
ROM1:1B92                 ldd     #$507           ; this may be an error code
ROM1:1B95                 rts
ROM1:1B96 ; ---------------------------------------------------------------------------
ROM1:1B96 CompareActualPicNumWithExpected:        ; CODE XREF: ThinkActualFrameNumIsExpected+3 j
ROM1:1B96                                         ; ThinkActualFrameNumIsExpected+21 j
ROM1:1B96                 clr     SlowPifCounter  ; may throttle how often we check for seek complete among other things
ROM1:1B99                 ldd     CurrentHexPicNumber ; Stores current laserdisc picture number in hex (converted from BCD).
ROM1:1B99                                         ; (see 6f3)
ROM1:1B99                                         ; Gets 50000 (decimal) stored if VBI frame number appears to be  invalid.
ROM1:1B99                                         ; (see 6E4)
ROM1:1B9C                 subd    ExpectedHexPicNum ; The picture number we expect to be on (used to detect successful searches, etc).
ROM1:1B9C                                         ; (see 1b9c)
ROM1:1B9F                 beq     ActualFrameNumMatchesExpectedFrameNum ; This gets cleared when the expected frame number matches the actual frame number. (see $1BDD)
ROM1:1B9F                                         ; This is used by the disc "search to" test to indicate that an expected frame number was detected by the VBI reader.
ROM1:1BA1                 bhi     ActualPicNumGreaterThanExpected ; are we off by more than 2 tracks?
ROM1:1BA3                 coma                    ; negate the delta (I am pretty sure, but I haven't tested it)
ROM1:1BA3                                         ; so it is positive instead of negative
ROM1:1BA4                 negb
ROM1:1BA5                 sbca    #$FF
ROM1:1BA7                 cmpd    #4              ; is delta greater than 4?
ROM1:1BAB                 bcs     SendSkipForwardCommand
ROM1:1BAD                 ldd     #4              ; assume delta is 4 if it is larger than 4 because we only have 4 skip forward commands available to us.
ROM1:1BB0 SendSkipForwardCommand:                 ; CODE XREF: ThinkActualFrameNumIsExpected+43 j
ROM1:1BB0                 pshs    b
ROM1:1BB2                 addd    CurrentHexPicNumber ; Stores current laserdisc picture number in hex (converted from BCD).
ROM1:1BB2                                         ; (see 6f3)
ROM1:1BB2                                         ; Gets 50000 (decimal) stored if VBI frame number appears to be  invalid.
ROM1:1BB2                                         ; (see 6E4)
ROM1:1BB5                 std     CurrentHexPicNumber ; Adjust current picture number under the assumption that the
ROM1:1BB5                                         ; track jump will be successful.
ROM1:1BB5                                         ; We need to track it in case we have to perform multiple track jumps in a row.
ROM1:1BB8                 puls    b
ROM1:1BBA                 addb    #$C             ; send one of 4 PIF 'skip forward' commands,
ROM1:1BBA                                         ; either 0xD, 0xE, 0xF, or 0x10
ROM1:1BBC                 stb     PifArray1ByteSpecial ; This byte will be sent to the pif board if it is non-zero.
ROM1:1BBC                                         ; It is special in that it can receive a higher priority to be sent (I don't fully understand it yet).
ROM1:1BBF                 clra
ROM1:1BC0                 rts
ROM1:1BC1 ; ---------------------------------------------------------------------------
ROM1:1BC1 ActualPicNumGreaterThanExpected:        ; CODE XREF: ThinkActualFrameNumIsExpected+39 j
ROM1:1BC1                 cmpd    #2              ; are we off by more than 2 tracks?
ROM1:1BC5                 bcs     SendSkipBackwardCommand ; save difference
ROM1:1BC7                 ldd     #2              ; Assume we are off by 2 if it's more than that.
ROM1:1BC7                                         ; This is because there are only two reverse PIF commands,
ROM1:1BC7                                         ; 0x11 and 0x12 so our difference value must be
ROM1:1BC7                                         ; either 1 or 2 since we add to 0x10 below.
ROM1:1BCA SendSkipBackwardCommand:                ; CODE XREF: ThinkActualFrameNumIsExpected+5D j
ROM1:1BCA                 pshs    a,b             ; save difference
ROM1:1BCC                 ldd     CurrentHexPicNumber ; Stores current laserdisc picture number in hex (converted from BCD).
ROM1:1BCC                                         ; (see 6f3)
ROM1:1BCC                                         ; Gets 50000 (decimal) stored if VBI frame number appears to be  invalid.
ROM1:1BCC                                         ; (see 6E4)
ROM1:1BCF                 subd    ,s
ROM1:1BD1                 std     CurrentHexPicNumber ; Adjust current frame number as if the track jump has already occurred and is successful.
ROM1:1BD1                                         ; We need to track this here in case we need to perform multiple
ROM1:1BD1                                         ; track  jumps in a row.
ROM1:1BD4                 puls    b,a             ; restore difference
ROM1:1BD6                 addb    #$10            ; this converts the difference value into one of the two PIF 'jump backward' commands (0x11 or 0x12).
ROM1:1BD8                 stb     PifArray1ByteSpecial ; This byte will be sent to the pif board if it is non-zero.
ROM1:1BD8                                         ; It is special in that it can receive a higher priority to be sent (I don't fully understand it yet).
ROM1:1BDB                 clra
ROM1:1BDC                 rts
ROM1:1BDD ; ---------------------------------------------------------------------------
ROM1:1BDD ActualFrameNumMatchesExpectedFrameNum:  ; CODE XREF: ThinkActualFrameNumIsExpected+37 j
ROM1:1BDD                 clr     ExpectedFrameNumStatus ; This gets cleared when the expected frame number matches the actual frame number. (see $1BDD)
ROM1:1BDD                                         ; This is used by the disc "search to" test to indicate that an expected frame number was detected by the VBI reader.
ROM1:1BE0                 lda     #$F
ROM1:1BE2                 sta     word_0_A1C0     ; Gets set to 0xF when expected pic number matches actual pic num.
ROM1:1BE2                                         ; (see 1be2)
ROM1:1BE5                 ldd     CurrentHexPicNumber ; Stores current laserdisc picture number in hex (converted from BCD).
ROM1:1BE5                                         ; (see 6f3)
ROM1:1BE5                                         ; Gets 50000 (decimal) stored if VBI frame number appears to be  invalid.
ROM1:1BE5                                         ; (see 6E4)
ROM1:1BE8                 std     SearchedHexPicNumber ; Gets picture number after a successful search.
ROM1:1BE8                                         ; (see 1be8)
ROM1:1BEB                 clr     byte_0_A1B8     ; Gets cleared when expected picture number matches actual picture number
ROM1:1BEB                                         ; (see 1beb)
ROM1:1BEE                 clra
ROM1:1BEF                 rts
ROM1:1BEF ; End of function ThinkActualFrameNumIsExpected

Again, this function was quite difficult to understand and took me a long time.

$1B68 checks to see whether it's time to read the real frame number from the video signal.

If it's time, it reads the frame number at $1B86 and converts it from the BCD laserdisc format to regular binary/hex format.

If the frame number cannot be converted from BCD because it contains 'hex' digits, or if it is not a valid VBI picture number, or if it times out, it will try to skip 4 tracks forward and retry ($1B8B).

$1B96 is the routine that actually compares the two frame numbers (actual vs expected).  The expected picture number will be subtracted from the actual picture number.  If the difference is 0, the search is assumed to be successful ($1BDD).  Otherwise, the program will attempt to _skip_ to the right location (which is pretty nifty).  It will either skip forward ($1BB0) or backward ($1BCA).

To fix Dexter, I probably need to make sure that I am sending correct picture numbers in the VBI signal a few fields/frames before I send the 'search has finished' signal.  This will allow the frame decoding hardware on the VGG board time to get a good read before the ROM program tries to validate whether it is correct.  Alternatively, I can make all seeks completely instantly for PR-8210A mode which is probably a lot safer but not as autentic.

Monday, October 26, 2015

Dragon's Lair Movie Kickstarter!

If you like laserdisc games (And I am assuming you do since you are reading this), go here and help support this project!

Sunday, September 20, 2015

Dexter Auto-detection of player type working in latest update

Dexter can now auto-detect whether a "Rick Dyer" game is configured for LD-V1000 or PR-7820 mode.  These games include Dragon's Lair, Space Ace, and Thayer's Quest.

Thursday, August 27, 2015

Astron Belt (VIP9500SG mode) confirmed working in Dexter!

I just finished up experimental support for the Hitachi VIP9500SG laserdisc player for Dexter.  (BTW, I learned that the 'SG' apparently stands for SEGA hehe).  Warren tested it out yesterday and it seems to mostly work.  There may be a few defects but frankly it's hard to tell what "working correctly" looks like on this game since it's so poorly designed :)

I'll be adding this new firmware update to Dexter Manager soon (it's not quite available).

Tuesday, August 25, 2015

Cliff Hanger confirmed working on Dexter rev 3d!

Cliff Hanger is now confirmed working on the latest Dexter rev 3d board using the infrared interface!  This is huge!!! :)

Saturday, August 15, 2015

Dexter Dragon's Lair 2 bug fixed

Attention Dexter users,

I've fixed a long-standing bug with Dragon's Lair 2 and Dexter where every seek would return with an error.  The game can now be played with Dexter (again) although I still have not implemented stop code support, so all scenes will overrun slightly.  It's progress!  You can update your Dexter devices using Dexter Manager (link on the front of the Dexter home page).

Sunday, August 9, 2015

Dexter HOWTO (updated as of 8 Aug 2015)

New Dexter HOWTO videos

I've been threatening to do these for what, 3 weeks?  I finally got them filmed.

Sorry that I speak quietly in some parts.  Hopefully I covered everything most people will need to get the most out of their Dexter boards.

Wednesday, July 22, 2015

Pre-built Dexter PCBs have arrived!

I was hoping to get these at CAX but it was a day late.  They were waiting for me when I got home from CAX.

I've tested them and they are basically everything I had hoped for!  I will be shipping these out to people who prepaid for them ASAP.

Here is a video of a quick test:

Thursday, July 9, 2015

How to reverse engineer arcade game hardware

I'll be giving a presentation at CAX again this year.  This time it will be about reverse engineering arcade game hardware.  I'll share some of the tricks I've learned over the years including how to read schematics, disassemble ROM images, use logic analyzers, etc.  I hope to see everyone there ;)

Monday, June 22, 2015

Reverse engineering Zany Golf (Apple IIgs version) just enough to be able to actually see the whole game :)

So yesterday was Father's Day, and I decided that I wanted to show my kids my old Apple IIgs games that I loved.  We pulled up Zany Golf but had a problem: my kids kept losing all their strokes (turns) immediately so they weren't having any fun!  I searched online for a cheat so that I could show them the whole game but found nothing.

Challenge accepted!

Tools used: KEGS debugger (setting breakpoints, etc), gdb, and IDA Pro

Why using an emulator is so much nicer: Using an emulator to reverse engineer these old pieces of software is so much more convenient than using a real Apple IIgs.  For one thing, it's a lot faster to start the game.  And for another things, its built-in debugger is much nicer for stepping through code than trying to hack the boot process of the game enough to do it with the built-in monitor (yuck!).

I decided to search for the message "Player 1, this is your last stroke." because I knew that somewhere near this message would be some logic to check to see what the current stroke count was.  And if I knew where the current stroke count was stored, I could set a breakpoint and catch when it gets changed (decremented).

First thing I did was searched for this string on disk.  I found it inside the file named "CODE" inside a Zany Golf subdirectory.  At this point, I realized that this wasn't very helpful because I did not know how CODE got loaded into memory.  I decided that I needed to run Zany Golf in KEGS and search the memory for this string to find where it got loaded.

Unfortunately, I discovered that KEGS (apparently!) does not have a memory search function.  After a few false starts down other paths, I decided to whip up a quick n' dirty memory search function that searches the entire emulated memory space for the string I was looking for.  On a modern PC, this brute force approach still completes very quickly so it's no big deal.

I tried to understand how KEGS actually managed the emulated memory and I got confused quickly.  As grateful as I am to have an emulator for the Apple IIgs, KEGS unfortunately is somewhat convoluted for me (although maybe others don't have as much trouble) with heavy use of global variables that look like local variables, and lots of macros.  So I ditched the idea of understanding how KEGS handles its own memory and instead just wrote a new search method that calls the get_memory_c method that KEGS provides and which the debugger uses to do memory dumps.  My custom method was tailored specifically to the search string and once I found the string, I wouldn't need it again.

Using this approach, I found the string at $01/736D.  On disk, it is at offset $726B inside the CODE file which is kinda weird.  This suggests that CODE is loaded at $01/0102.  I used IDA Pro to disassemble CODE at this starting address to give me some more visibility.  This succeeded with partial success although a lot of the 8/16 bit addressing modes were fouled up.

So the next task was to find when this memory location got read.  Since KEGS did not provide a memory breakpoint feature, I decided to use gdb instead.  First I had to find out where the native memory location for this emulated memory location was.  So I set a breakpoint on get_memory_c and inside KEGS debugger, I type "01/736d" so that only that memory location would grabbed.

I stepped through gdb at this point and after the GET_MEMORY8 macro was called, I typed "print ptr" to see the native memory location.

It came back as 0x7ffff647746d (ie a random number) and this value isn't that important because it may change each time the emulator is run (at least that is my impression).  What is important is that I set a memory breakpoint on it and then get the string about my last stroke to appear by playing through the game.

So I typed "rwatch 0x7ffff647746d" and then "continue" to return control to KEGS.  I then typed "g" to exit KEGs debugger and resume the game.  I played the game until I had one stroke left and sure enough, my memory breakpoint got hit! Woohoo!

'kpc' is the variable name inside KEGS that holds the Program Counter (PC) so I typed "print kpc".

kpc was set to $13e22 (which is $01/3e22).  Finally, a starting point to start stepping through the code!

I set a breakpoint on $01/3e20 and restarted the game.  But the breakpoint was getting hit too often so I concluded that it was part of the "Draw text" routines.  So I disabled the breakpoint, started a new game, and played until right before the message was to appear.  Then I entered KEGS debugger again and set the breakpoint.

I putted the ball and sure enough the breakpoint got hit again.  I set a new breakpoint on the RTS instruction which came shortly thereafter (because KEGS does not have a way to 'step out' of a function, so this is a poor man's equivalent).

This took me to $01/440C which made the JSR, but this is the beginning of a subroutine (according to IDA Pro) so I concluded that nothing interesting was here.  I set breakpoint on 4428 (right before RTS) to step out of this method.

This took me to $01/4437, which is still a small subroutine and did not appear to do any logic to check to see if it's the last stroke.  I set breakpoint on $01/4440 to step out of the function.

This took me to $01/6da2 which modifies $28 and $29 so this may be the subroutine that sets the message about it being the last stroke, as I noticed some references to these constants earlier.

I set a breakpoint at $01/6deb, right before the next RTS to step out of this method.

This took me to $01/73cb.

Ah ha!  I did a disassembly a little before this point and saw that $01/73b4 does "LDA $424E,X" and "CMP #1" and "BNE $73CB".  This is exactly what I've been looking for: something that checks to see if a value is equal to 1.  Therefore, $01/424E,X must hold the strokes for the current player!

Looking a little further back, at $01/73b1, I saw LDX $4218 right before the LDA $424E, X ; so $4218 must hold the current player (I assume it would be 0-3).

Returning from this function put me at $01/7473.  I saw that the previous call was JSR $7390.

I did not see anything that decrements the memory location jumping out at me so I went back to gdb's data breakpoint on this memory location ($01/424E) using the same technique that I described earlier.

Without even needing to start the game over again, I putted the ball again (which concluded with my game being over) and the memory breakpoint was hit... ($01/705E)


Here's the code that decrements the stroke count! :)

ROM:7058                 LDX     word_4218
ROM:705B                 LDA     $424E,X
ROM:705E                 DEC
ROM:705F                 STA     $424E,X

Looks like a simple NOP at $705E should do the trick to give unlimited strokes.

Using a sector editor, such as Copy II Plus, search for AE 18 42  BD 4E 42  3A  9D 4E 42 (I found it on block $40)

Change the 3A (DEC) to EA (NOP)

You can now show your kids this game without making them get frustrated!! :) :) :)

I've tested through level 2 and have no idea if something REALLY BAD will happen if the stroke count gets too high later, so take that as a caveat.  Possible changes to this mod are to hard-code the stroke count to something like 5 instead of NOP'ing out the decrement.


UPDATE 23 Oct 2015:

I wanted to try this trick again on a different disk but had forgotten how I hacked KEGS to do a memory search.  Here is the function I wrote for reference:

void find_zany()
        unsigned char array[] = { 0x50, 0x6C }; // "Pl"
//      unsigned char array[] = { 0x50, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x20 };   // "Player "
        unsigned char cmp[sizeof(array)];
        int iBank = 0;
        int iAddr = 0;
        int i = 0;

        // search all reasonable banks where it could be
        for (iBank = 0; iBank < 9; iBank++)
                printf("Checking bank %02x\n", iBank);
                for (iAddr = 0; iAddr < (0xFFFF - sizeof(array)); iAddr++)
                        for (i = 0; i < sizeof(array); i++)
                                cmp[i] = get_memory_c((iBank << 16) | (iAddr + i), 0);

                        // if we get a match
                        if (memcmp(array, cmp, sizeof(array)) == 0)
                                printf("Found a match at bank %02x, addr %04x\n", iBank, iAddr);

Inside dis.c, inside the function called do_debug_intfc(), I added this to the switch statement:

case 'k':       // zany golf