Posts: 120
Threads: 8
Joined: Apr 2012
Reputation:
5
did you try to see the generated asm (-S flag during compilation?) to verify that used registers are all saved and restored correctly in the vbl? and check also if bank register 3 is used in the generated code as it seems to be what system calls are using.
just an idea...
Posts: 120
Threads: 8
Joined: Apr 2012
Reputation:
5
generated asm seems ok, registers saved in stack and restored, no bank 3 register used.
"vgm send data" may take too much time in the interrupt, maybe removing it from vbl and call it after Sleep may help
Posts: 99
Threads: 16
Joined: Apr 2020
Reputation:
8
(06-19-2026, 12:47 AM)sodthor Wrote: generated asm seems ok, registers saved in stack and restored, no bank 3 register used.
"vgm send data" may take too much time in the interrupt, maybe removing it from vbl and call it after Sleep may help
I have the vgm driver synched to vbl. It transfers an entire frame of sound data for each channel for bgm and/or sfx to shared memory during vbl and triggers the z80 to use it. It could be slowing things down when all channels are in use. I wonder, if it's going to be possible to have it coexist. It would be limiting, but the z80 driver could be configured to use a larger amount of shared memory, and sfx and bgm data gets preloaded, like the snk driver and pgs driver do.
Do we have control over the timing mechanism for serial? Is vbl the best synching interval?
Does serial try to send the full write payload and read full payload within one frame? Can they, or do they alternate each frame? Seems like one system should be the host and the other the guest and the host sends requests and its own data in the request
Hmmm... I'm trying to think about what kind of data the 2 systems would have to synch. At a minimum they may just need to tell each other the current user inputs and each system knows how to handle everything else on its end.
I've worked a lot with serial devices and communication protocols, but less at the bare metal level. Given a guest/host, the baud rate is the same between the 2, and they take turns with the host sending requests and data payloads and the guest sending replies.
I guess I'm not sure on NGPC how much is handled by the hardware vs how much you have to be directly involved in reading bit by bit.
I recall seeing an official snk document on linking at one point, but not sure how helpful it is.
Posts: 99
Threads: 16
Joined: Apr 2020
Reputation:
8
06-19-2026, 06:33 AM
(This post was last modified: 06-19-2026, 08:34 AM by winteriscoming.)
Here's the official doc:
https://neogeopocket.es/wp-content/uploa...ialCom.PDF
So there is a lot taken care of directly by the hardware, making it easier.
Here's some relevant info:
Any operation which requires a long operation period and suffers from multiple interrupts such as V-BLANK
should have VECT_COMOFFRTS before the operation and VECT_COMONRTS before reti
command.
VECT_COMGETDATA and VECT_COMGETBUFDATA can go into main.
You could conceivably tap into the serial lines and record what's happening between two systems in a commercial game.
Serial specs:
Communication speed : 19200bps
Bits : 8 bit
Parity : None
Stop bit : 1 bit
Handshake flow : handshake with CTS, RTS signals
Posts: 99
Threads: 16
Joined: Apr 2020
Reputation:
8
It reads to me as if the RTS/CTS handshake are baked into the built in commands. So each system is responsible for setting its RTS to off when communication should pause, and back on when communication can continue. The send commands don't send when the linked system is not ready to receive. It's not clear to me if one system simply waits for the other to be ready before continuing. Like, would one system just halt execution until the other is ready? Maybe?
I guess the idea is that it can pause communication during interrupts. So maybe rts off at the beginning of vblank and rts on at the end would allow the vgm player to work as is? It would get handled when communication is paused.
Posts: 97
Threads: 13
Joined: Feb 2018
Reputation:
4
Thanks both - that's definitely something to work with - I've parked this for a while now, but will pick it up when I can
My code certainly isn't optimised at the moment so I've got a bit of work to do on that score anyway. Currently I'm testing the entire playfield within each frame which is silly as that takes a couple of vblanks on it's own.
The point about host/client is well worth thinking about - I'm currently assigning a host/client role to each side of the link, but in this proof of concept there is no difference in the behaviour between the two.
My comms packet at the moment is "just" a 50 byte representation of the local playfield - I'm aiming at a max 128 bytes in total which would include the sprite buffer, score and other game info. The current method of essentially just sending 50 individual bytes and then waiting for 50 bytes in return is NOT optimal and I suspect is the problem here. I'd hope that if/when I get the buffer transfer working properly that that would help it
Posts: 120
Threads: 8
Joined: Apr 2012
Reputation:
5
06-19-2026, 11:22 PM
(This post was last modified: 06-24-2026, 09:36 PM by sodthor.)
A faster BlockCopy (if n > 0):
void BlockCopy(u8 * Dest, const u8 * Source, u16 n)
{
__asm(" ld XDE,(XSP+0x4)");
__asm(" ld XHL,(XSP+0x8)");
__asm(" ld BC,(XSP+0xc)");
__asm(" ldir (XDE+),(XHL+)");
}
with n == 0 check:
ld BC,(XSP+0xc)
cp BC,0
ret z
ld XDE,(XSP+0x4)
ld XHL,(XSP+0x8)
ldir (XDE+),(XHL+)
instead of generated asm :
_BlockCopy:
ld XHL,(XSP+0x4)
ld XDE,(XSP+0x8)
ld BC,(XSP+0xc)
ld WA,BC
dec 0x1,BC
cp WA,0x0
ret eq
L78: ;2
ld A,(XDE+:1)
ld (XHL+:1),A
ld WA,BC
dec 0x1,BC
cp WA,0x0
j ne,L78
L77: ;1
ret
but memcpy from the compiler lib is even more optimized to use ldirw ( https://github.com/sodthor/ngpcdev/blob/...2/memcpy.c)
Posts: 97
Threads: 13
Joined: Feb 2018
Reputation:
4
Tixu (the ngpcraft guy or gal) has been helping me with this - my single byte copy might actually be faster, but his is definitely more robust
https://codeberg.org/ahchay/FRUITYVINES.ngp
Two includes, and one change to use ngpc.h and it works like a charm - two lines to init:
Code: ngpc_linkkit_init();
link_state=ngpc_linkkit_state();
link_state isn't needed it to avoid unnessecary screen refreshes when updating the status graphics
Then the actual meat of the transfer is
Code: // Setup an 8 byte transfer buffer
// This one contains the row number to update and the 5 individual tile ID's for my puzzle game
myBuffer[0]=cur_LinkRow;
for(iLoopX=0;iLoopX<5;iLoopX++)
{
myBuffer[1+iLoopX]=Board[cur_LinkRow][iLoopX].TileNo;
}
// Just resets the current row indicator so that it loops from 0-9
cur_LinkRow++;
if(cur_LinkRow>9) cur_LinkRow=0;
ngpc_linkkit_stage(myBuffer); // stages the send
ngpc_linkkit_update(); // sends and receives data
if (ngpc_linkkit_fresh()) // Checks that the link is active
{
u8 TargetRow;
ngpc_linkkit_peek(TheirBuffer); // Retrieves the data into gRx[]
// Copy the buffer from the "other" machine into the game structures
TargetRow=TheirBuffer[0];
PrintDecimal(SCR_BACK_PLANE,PAL_TEXT,0,1,TargetRow,2);
for(iLoopX=0;iLoopX<5;iLoopX++)
{
OpponentBoard[TargetRow][iLoopX].TileNo=TheirBuffer[1+iLoopX];
}
}
And that's kind of it - you have to limit the buffer size to a miserly 8 bytes - hence I copy my playfield over one line at a time. I could probably compress that a bit and do it fewer chunks, but it's fine for puzzle games...
If you want to have some kind of onscreen indicator of the link status, you just query ngpc_linkkit_state()
Code: if(link_state!=ngpc_linkkit_state())
{
link_state=ngpc_linkkit_state();
switch (ngpc_linkkit_state()) // Check link status
{
case LINKKIT_CONNECTING:
//PrintString(SCR_BACK_PLANE,PAL_TEXT, 0, 0, "LOOKING FOR PLAYER 2");
PaintTile(SCR_BACK_PLANE,384+LINK_HOST,PAL_LINK_HOST,7,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_CABLE,PAL_LINK_CABLE,9,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_CLIENT_DISCONNECTED,PAL_LINK_HOST,11,0,2,2);
break;
case LINKKIT_READY:
//PrintString(SCR_BACK_PLANE,PAL_TEXT, 0, 0, ngpc_linkkit_role() == LINKKIT_ROLE_HOST ? "CONNECTED - HOST " : "CONNECTED - GUEST ");
PaintTile(SCR_BACK_PLANE,384+LINK_HOST,PAL_LINK_HOST,7,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_GOOD,PAL_LINK_GOOD,9,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_CLIENT,PAL_LINK_HOST,11,0,2,2);
break;
case LINKKIT_LOST:
//PrintString(SCR_BACK_PLANE,PAL_TEXT, 0, 0, "LINK LOST - RETRYING");
PaintTile(SCR_BACK_PLANE,384+LINK_HOST,PAL_LINK_HOST,7,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_RETRY,PAL_LINK_QUESTION,9,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_CLIENT,PAL_LINK_HOST,11,0,2,2);
break;
case LINKKIT_MISMATCH:
//PrintString(SCR_BACK_PLANE,PAL_TEXT, 0, 0, "OTHER CART DIFFERS ");
break;
default:
//PrintString(SCR_BACK_PLANE,PAL_TEXT, 0, 0, "NO LINK ");
PaintTile(SCR_BACK_PLANE,384+LINK_HOST,PAL_LINK_HOST,7,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_BAD,PAL_LINK_BAD,9,0,2,2);
PaintTile(SCR_BACK_PLANE,384+LINK_CLIENT,PAL_LINK_HOST,11,0,2,2);
break;
}
The game itself needs a bit more finesse - as far as link mode goes though, it should probably wait for the second player to join before starting to play, and should also quit if the link is interrupted or whatever
Posts: 97
Threads: 13
Joined: Feb 2018
Reputation:
4
Also https://codeberg.org/ahchay/HIGHNOON.ngp - a simple multiplayer test of the Outlaws game
Just testing how it copes with "action" - so that's 3 bytes for the player (position and direction/action) and 3 bytes for the bullet (position and direction) each frame
Testable in the NGPCraftg emulator - https://github.com/Tixul/Ngpcraft_emulator
Posts: 120
Threads: 8
Joined: Apr 2012
Reputation:
5
|