Forums

Sega Master System / Mark III / Game Gear
SG-1000 / SC-3000 / SF-7000 / OMV
Home - Forums - Games - Scans - Maps - Cheats - Credits
Music - Videos - Development - Hacks - Translations - Homebrew

View topic - Light phaser code/algorithm

Reply to topic
Author Message
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Light phaser code/algorithm
Post Posted: Fri Feb 15, 2019 8:45 pm
I've been looking at some games' light phaser code and the official Sega docs, and I'm finding it quite confusing.

The docs say that the TH signal is pulsed for 20-30us when the gun sees light. This seems not to be a feature of the gun, so presumably it's the VDP (or IO chip?) analysing the gun signal and denoising/edge detecting/pulse generating - but if so, why only 20us? That's only ~70 CPU cycles. Why not set a per-line flag?

The docs also say that the H position is latched when the edge is seen. Therefore the code must poll TH, grab the H and V positions, and the values returned will trace the left edge of a circle "seen" by the light sensor. The game must then calculate where it thinks the gun is pointing (the middle of the circle) based on this.


So I disassembled Missile Defense 3D. It does a few interesting (and possibly unexplained) things:

1. It polls TH much faster than the docs say is needed. Every 25 cycles (7us), in fact. This doesn't leave time for it to ever exit the polling loop, so it does some funky call stack rewriting in the frame interrupt to terminate the loop.
2. It has what look like delay opcodes between each time it gets triggered, so it won't see points on the same line - but then it also uses the V count to discard points seen on the same line
3. It discards the first two lines it sees
4. It seems to compute the X position as the mean of the points it sees, which ought to mean it mis-tracks to the left of the cursor
5. It scales and offsets the values it gets from the H counter, suggesting the values don't map to the pixel count

It's possible this is just horrible code that happens to come out with a reasonable result because one part (the skew to the left) cancels the other (the scaling/offsetting) in normal use cases. Certainly when I played it a few years ago on a small CRT it seemed OK.

Averaging might be reasonable if the TH signal was a raw "do I see light" signal, and you polled really fast, because then you'd aim to sample points all over the circle and averaging them will be sensible. But the code is careful to ignore multiple points on the same scanline.


I also had a look at Porkopolis. This seems to act entirely differently (if my understanding is correct):

1. It only checks TH in the HBlank interrupt
2. If low, it grabs the H and V counts
3. It then doubles the H count and subtracts 32 to map it to pixel space

If the TH signal is only held low for 30us, then I don't see how this can work on a real system (where each line is ~63us) - but forum posts suggest it does. Can someone confirm that?


Does anyone have any other insight? I thought maybe I should have a look at another game, maybe Missile Defense 3D is not "normal". I am thinking of making a test ROM, but I don't actually have any hardware to test it with.
  View user's profile Send private message Visit poster's website
  • Joined: 14 Mar 2018
  • Posts: 19
Reply with quote
Post Posted: Sat Feb 16, 2019 9:57 am
I can't speak for the MS version, but I've been doing some C64 light gun coding. There is a delay in the signal, once you press the trigger and/or it sees the light beam, the signal takes time to travel down the cable. While on say the pad, this amount of time in tiny and trivial, when you are measuring the pixel clock, its a significant delay. And such you will get a H reading that is further to the right than where the user is aiming. Also hands are shaky and need to be factored in.
  View user's profile Send private message
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Sat Feb 16, 2019 10:37 am
That's certainly true, and I expect the alignment of sensor is going to be fairly dodgy, but I guess I still need to make a test ROM to collect the raw data from a real system to see what the gun really "sees".
  View user's profile Send private message Visit poster's website
  • Joined: 14 Mar 2013
  • Posts: 62
  • Location: Belgium
Reply with quote
Post Posted: Wed Feb 20, 2019 2:29 pm
Every filter in between the SMS and light gun input adds delay too. It's not a direct connection from light sensor to an input as shown by the schematic that someone drew in a previous post : http://www.smspower.org/forums/files/lightphaser3050_145.png


It's interesting to see how different games use the light gun.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Wed Feb 20, 2019 9:21 pm
I'm hoping to find a standard function in several games... but it's clearly not a simple copy paste job.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Tue Feb 26, 2019 10:02 am
Last edited by Maxim on Wed Feb 27, 2019 10:22 am; edited 2 times in total
Here's a commented/reformatted/refactored version of the sample code in the Sega docs for reading the light phaser...

Quote
.enum $c100
  GunState db ; Gun Status Work (1)
  GunDataCount db ; RAM Save Counter (1)
  GunData dsb 32 ; V.H Counter Save Work (40H)
  BlobCount db ; (1)
  BlobHeights dsb 10 ; (10)
  ShotFired db ; Gun Shot Flag (1)
  GunX db ; H.Position Data (1)
  GunY db ; V.Position Data (1)
  ButtonsPressed db ; Switch Data (2)
  ButtonsJustPressed
.ende

.define PORT_VDP_ADDRESS $BF
.define PORT_VDP_DATA $BE
.define PORT_IO_A $dc
.define PORT_IO_B $dd
.define PORT_H_COUNT $7f
.define PORT_V_COUNT $7e

;=======================================================
;
; ****** GUN SHOOT ADDRESS SEARCH *****
;
;=======================================================

PollGun:
  ld hl, GunDataCount
  ld de, GunData
  ld c, 32

PollGunExitPoint:
  ; Check state is 1
  ld a, (GunState)
  dec a
  ret nz
 
--: ; TH polling loop
  ; Wait for TH
  in a, (PORT_IO_B)
  and 1<<6
  jp nz, -
  ; If we have filled the buffer, don't do anything more
  ld a, (hl)
  cp c
  jr nc, -
  ; Increment data counter
  inc (hl)
  ; Save H and V to RAM
  in a, (PORT_H_COUNT)
  ld (de), a
  inc de
  in a, (PORT_V_COUNT)
  ld (de), a
  inc de
  ; Wait for TH to be non-zero
-:in a, (PORT_IO_B)
  and 1<<6
  jp z, -
  jp --

;=======================================================
; ***** V & H DATA CHECK *****
;=======================================================
; Not referenced, presumably called once per frame?
GunProcessData:
  ; If no shot was fired, nothing to do
  ld a, (ShotFired) ; Gun Shoot Flag
  or a
  ret z
 
  ; Clear the flag
  xor a
  ld (ShotFired), a

  ; Fewer than 5 data points -> ignore them
  ld a, (GunDataCount) ; V & H Counter
  cp 5
  ret c

  ; Save counter-1 in b
  dec a
  ld b, a
  ; Point at V counts
  ld hl, GunData+1
  ld e, 0
-:  ld a, (hl) ; Get a value
    inc e
    ld c, a
    inc hl
    inc hl
    xor a
    ld a, (hl)  ; Get next one and compare
    sub c
    cp 3
    call nc, _AddGunBlob ; Gap is >=3
  djnz -
  call _AddGunBlob

  call _FindLargestBlob

  ; Get the H value
  ld a, (hl) ; h-counter read
  ; If >$a0, ignore it
  cp $a0
  ret nc
  ; Subtract 22
  and a, a
  sub 22

  ; *2 -> B
  sla a
  ld b, a
  ; /64
.repeat 5
  rra
.endr
  and a, 7
  add b
  ; H position = (x - 20) * 2.03125 where x is the reported H count for the centre of the blob
  ld (GunX), a
  inc hl
  ld a, (hl) ; v.counter read
  and a, a ; carry flag = clear
  sub 1
  ld (GunY), a
 
  ; Restore the palette (all black here)
  xor a
  out (PORT_VDP_ADDRESS), A
  LD A, $C0
  OUT (PORT_VDP_ADDRESS), A
  LD BC, 32 << 8 | PORT_VDP_DATA
  LD HL, _Palette ; Color Data Table
  OTIR
  RET
_Palette:
.dsb 32 0

_AddGunBlob:
  ; Copies e to BlobHeights[BlobCount]
  ; This is the index of the last seen run of V counter parts
  ; The code limits to 5 such runs (although the RAM area is big enough for 10)
  push hl
    ld hl, BlobCount
    ld a, (hl)
    cp 5
    jp nc, +
    inc (hl)
    ld hl, BlobHeights
    push de
      ld d, 0
      ld e, a
      add hl, de
    pop de
    ld (hl), e
+:pop hl
  ld e, 0
  ret


-:; Only one "chunk" seen, just use that one
  ld a, (hl)
  jp ++

_FindLargestBlob:
  ; Point at the work data array
  ld hl, BlobHeights
  ld c, 0
  ld a, (BlobCount)
  cp 1
  jp z, -

  ; More than one blob, so we need to choose one
  ld b, a
--  ld a, (hl) ; Get run length
--: inc hl
    cp (hl)    ; compare to following
    jp c, +    ; if following is larger, increment c and use it as the next reference
    djnz --
  jp ++

+:  inc c
    djnz --

++:
  ; At this point we should have selected the tallest seen blob (? seems buggy)
  ; We divide its length by 2...
  rrca
  and $7f
  ; Shuffle blob length to c, blob index to a and b
  ld b, c
  ld c, a
  ld a, b
  ; If the index is 0, we can skip some work
  or a
  jp z, +
 
  ; Else we work through the lengths to find the V count data for the index we selected
  ld hl, BlobHeights
  xor a
-:  add a, (hl)
    djnz -
  add a, c
-:; Now a = the index of the H,V pair at the vertical centre of the lob we chose
  ; We point hl to that...
  add a, a
  ld c, a
  ld b, 0
  ld hl, GunData
  add hl, bc
  ret
+:; First blob, no looping needed
  ld a, c
  jp -




;=======================================================
;
; ***** GUN CHECK *****
;
;=======================================================
GunFrameHandler: ; Not referenced anywhere, presumably called in frame interrupt?
  ; Check state variable
  ld hl, GunState
  ld a, (hl)
  ld (hl), 2
  dec a
  ret z                  ;(GunState) = 1 ---> (GunState) = 2 , RET
  ld (hl), 0
  dec a
  ret z                  ;(GunState) = 2 ---> (GunState) = 0 , RET

  ; Else GunState = 0 = waiting for trigger
  call _CheckTrigger
  and 1<<4  ; Check for trigger
  ret z

  ; Reset counter
  xor a
  ld (GunDataCount), a
  ; Set state to 1 = blank screen
  ld a, 1
  ld (GunState), a
  ld (ShotFired), a
  ; Blank the palette
  xor a
  out (PORT_VDP_ADDRESS), a
  ld a, $c0
  out (PORT_VDP_ADDRESS), a
  ld a, $3f ; White
  out (PORT_VDP_DATA), a
  ; Clear the gun data buffer and associated memory - GunDataCount up to BlobHeights inclusive
  ld hl, GunDataCount
  ld de, GunDataCount+1
  ld bc, $01+$40+$01+10
  ld (hl), 0
  ldir
  ret




_CheckTrigger:
  ; Get inputs
  in a, (PORT_IO_A)
  and 1<<4 ; mask to button 1 only
  ld hl, ButtonsPressed
  cpl
  ld c, a ; data save
  xor (hl)
  ld (hl), c ; new sw.data save
  inc hl  ; => ButtonsJustPressed
  and c
  ld (hl), a
  ret






;=======================================================
; *** Interrupt Jump Check ***
;=======================================================
.org $38
  push af
    ld a, (GunState) ; If the state is 1...
    dec a
    jr nz, +
    ; We assume we were polling the gun and we want to break that loop.
    ; We discard af
    ex (sp), hl
    pop hl
    ; And replace the return address with PollGunExitPoint
    ld hl, PollGunExitPoint
    ex (sp), hl
  push af
+:pop af
  jp int38 ; normal frame interrupt

It:

1. Waits for the trigger to be pressed
2. Blanks the screen to white
3. Polls TH, waiting for it to be 0
4. Captures the H and V counts to a buffer (unless the buffer is full)
5. Polls TH, waiting for it to be 1, before going back to 3
6. When the frame is up, it tries to rewrite the stack (but I think it's buggy...)
7. It then looks through the data it captured and segments it into runs of H, V pairs where the V counts are <3 lines apart
8. It selects the longest run and then the H, V in the middle of that run
9. If H >= $a0, the gun is not pointing at the screen
10. Else X = (H - 22) * 33/16 and Y = V - 1

The code seems untested and the algorithm seems oddly tolerant of missing lines or triggers on the same line, tries quite hard to deal with "seeing" multiple things (which I don't see how it is possible to do), and then totally fails to try to compute a valid X coordinate anyway.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Tue Feb 26, 2019 9:51 pm
I disassembled Marksman Shooting / Trap Shooting / Safari Hunt and it actually uses almost exactly the same code as the official docs sample above. The byte sequence for the part where it scales the H-counter to an X coordinate is found in these games:

* Hang On & Safari Hunt
* Hang On & Safari Hunt [BIOS]
* Marksman Shooting & Trap Shooting & Safari Hunt
* Marksman Shooting & Trap Shooting.sms

...suggesting they all use the same code as their source.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Wed Feb 27, 2019 9:38 am
Last edited by Maxim on Wed Feb 27, 2019 12:23 pm; edited 2 times in total
Gangster Town is almost identical, except it subtracts 24 from the H count before scaling it up and it rounds the result to a multiple of 8 pixels.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Wed Feb 27, 2019 9:55 am
Similar code (subtracting 22 from the H counter) with a single opcode optimisation (sla a -> add a, a) is in:

Assault City [Light Phaser]
Rambo III
Rescue Mission
Wanted
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14682
  • Location: London
Reply with quote
Post Posted: Wed Feb 27, 2019 10:15 am
Laser Ghost is similar again, but with some changes to the code:

* It does not rewrite the stack to break the polling loop, instead it uses the V count to decide to stop polling the gun
* It reads the H and V counts after seeing a 1 -> 0 -> 1 transition on TH, the earlier code captures them on the 1 -> 0 transition - but as I understand it this ought to work much the same.
* It subtracts 27 from the H count instead of 22
* Then it clamps the result to the range 0..112
* It applies the same scaling, then subtracts another 16 at the end
* It takes Y = V + 4

So still no intelligent way to compute X, just (different) fudge factors which result in an X offset of about 5.7 pixels.
  View user's profile Send private message Visit poster's website
  • Joined: 28 Mar 2002
  • Posts: 180
  • Location: Toronto, Canada
Reply with quote
Post Posted: Wed Mar 15, 2023 3:07 am
Last edited by Rene Dare on Wed Mar 15, 2023 8:22 pm; edited 1 time in total
Maxim wrote

If the TH signal is only held low for 30us, then I don't see how this can work on a real system (where each line is ~63us) - but forum posts suggest it does. Can someone confirm that?

Does anyone have any other insight? I thought maybe I should have a look at another game, maybe Missile Defense 3D is not "normal". I am thinking of making a test ROM, but I don't actually have any hardware to test it with.


I've recently got a Canadian/US Master System in pristine condition (lucky me). Since I want to play light phaser games, I've also had to purchase a CRT TV and ended up getting a 32" Sony Wega (not the hi-scan version) which I expected to behave just like any other CRT TV.

However, on the first try of Safari Hunt, I've noticed the hits were slightly off to the right, indicating some level of input delay introduced by these newer CRTs that borderline makes the game unplayable.

Then yesterday I had some time to spare and ended up quickly disassembling Gangster Town and manually adjusting the HCounter readings (by subtracting a fixed amount) to offset the delay introduced by the TV and it did work surprisingly well. Gangster Town shows on the screen where the gun was pointing, making it easier for me to find the appropriate offset value (in my case, $0c).

That being said, I believe the 20-30 usec is the average time the gun will "see light" on each line. The connection from the light sensor to the TH pin goes through several amp steps and TTL level converter, which probably introduces an extra delay.

@Maxim, now that I have a proper system (I've been wanting to grab a full system with console and CRT for years) I will be more than happy to run homebrew test programs if you need.
  View user's profile Send private message
  • Joined: 05 Sep 2013
  • Posts: 3757
  • Location: Stockholm, Sweden
Reply with quote
Post Posted: Wed Mar 15, 2023 8:48 am
Rene Dare wrote
However, on the first try of Safari Hunt, I've noticed the shots were slightly offset to the right, indicating some level of input delay introduced by these newer CRTs that borderline makes the game unplayable.


I remember I had some similar problem with a CRT that had a DNR (Digital Noise Reduction) system. I turned that off and it disappeared.
  View user's profile Send private message Visit poster's website
  • Joined: 28 Mar 2002
  • Posts: 180
  • Location: Toronto, Canada
Reply with quote
Post Posted: Wed Mar 15, 2023 8:26 pm
sverx wrote

I remember I had some similar problem with a CRT that had a DNR (Digital Noise Reduction) system. I turned that off and it disappeared.


I've tried playing around with the TV service menu (there are hundreds of parameters you can change) but so far I couldn't find anything other than geometry settings.

The code change to adjust the HCounter reading is working really really well, so I'm quite happy with this.
  View user's profile Send private message
  • Joined: 05 Sep 2013
  • Posts: 3757
  • Location: Stockholm, Sweden
Reply with quote
Post Posted: Thu Mar 16, 2023 1:59 pm
I didn't have to use any service menu, it was a key on the remote to switch that off IIRC...
  View user's profile Send private message Visit poster's website
Reply to topic



Back to the top of this page

Back to SMS Power!