NET: full networked mission runs end-to-end; fix blank VDB heads (NIC/VDB port clash)

Console queues a mission -> egg over the wire -> pod loads + runs it -> all
cockpit heads render -> mission ends on the console timer. Four fixes:

1. ne2000.cpp: don't BX_PANIC on the TCR inhibit-CRC / auto-tx-disable bits.
   A full production boot runs the packet driver's internal loopback
   self-test, which sets those bits; Bochs' NE2000 aborted the whole
   emulator. Record them in TCR state instead (harmless for an emulated NIC;
   pcap/host does framing+CRC). Also fixes a latent coll_prio bit-4 vs bit-3
   round-trip bug. Committed copy in vpx-device/.

2. Confirmed the mission-load page fault (00FF:219D) was RIO-OFF; booting
   with the RIO live (net_full.conf) loads and runs the mission cleanly.

3. net_loop.conf + net-boot/loop.bat: GO.BAT-style loop so the pod stays
   connected to the console and picks up missions as they're queued (the
   real pod relaunches netnub after each mission/idle disconnect).

4. HEADLINE: blank MFD/radar heads were an I/O port collision. The VDB
   video splitter is hardwired at 0x300-0x31A (palettes 0x300/0x308/0x310);
   the NE2000 at nicbase=300 swallowed the game's VDB palette writes ->
   vdb_pal stayed zero -> pal_draw decoded every head to black. Same VDB
   spam corrupted NIC RX, dropping the console EndMission (mission overran
   its timer). Fix: move the NIC to 0x340 (DOSBox nicbase=340 + DOS NET.CFG
   PORT 340; must agree). VDB keeps 0x300. Config-only, no rebuild.

Adds: net_full.conf, net_loop.conf, net-boot/ (ODI drivers, NET.CFG@340,
loop.bat, README), vpx-device/{ne2000,ethernet_pcap}.cpp; updates NET-NOTES.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-05 21:18:42 -05:00
co-authored by Claude Fable 5
parent e12bfe1d3a
commit 9536d4cbc0
11 changed files with 2224 additions and 0 deletions
+47
View File
@@ -372,6 +372,53 @@ so this may not reproduce there. NEXT: retry with RIO enabled + DOSBox
ACK deadline during load); if it still faults, disassemble around game
code 00FF:219D. This joins the Division-renderer/crash workstream.
## FULL NETWORKED MISSION WORKS END TO END (2026-07-05, later) — all fixed
A complete networked mission now runs start to finish: console queues a
mission -> egg over the wire -> pod loads it -> RIO live -> **all cockpit
heads render** -> mission ends on the console timer. Four fixes got here:
1. **Mission-load crash was RIO-OFF** (confirmed). Booting with the RIO
enabled (`net_full.conf`: serial1 COM1 rxpollus:100 rxburst:16, full
RIO+sound production boot) — the page fault at 00FF:219D is gone and the
mission loads + runs. Real pods run RIO-on, so this was the whole thing.
2. **NE2000 `BX_PANIC` fixed in the emulator.** A full production boot runs
the packet driver's internal loopback self-test, which sets the NIC's TCR
inhibit-CRC bit; Bochs' `ne2000.cpp` `BX_PANIC`'d and aborted the whole
emulator. Patched the TCR write handler to record crc_disable/ext_stoptx
instead of panicking (harmless for an emulated NIC; pcap/host frames+CRCs).
Committed copy: `vpx-device/ne2000.cpp`.
3. **Pod persistence via GO.BAT loop.** When the console has no mission
queued, the pod connects, sees nothing, and cleanly exits (not a crash) —
the real pod's GO.BAT immediately relaunches netnub. `net_loop.conf` +
`net-boot/loop.bat` replicate that so the pod stays connected and ready.
4. **HEADLINE FIX — blank cockpit heads = NE2000/VDB I/O port collision.**
All 5 MFD + radar heads decoded to pure black during a live mission even
though the DOSBox SVGA gauge framebuffer was FULL (captured mid-mission:
SENSOR CLUSTER/MYOMERS/SRM 4/mech wireframes/pilot name). Root cause: the
VDB video splitter board is hardwired at **0x300-0x31A** (palettes
0x300/0x308/0x310; `VDB_BASE` in vpxlog.cpp) and we'd put the NE2000 at
`nicbase=300` — the NIC swallowed the game's VDB palette writes, so
`vdb_pal` stayed zero and `pal_draw` mapped every index to black. The VDB
spam into 0x300-0x31A also corrupted NIC registers, dropping the console's
EndMission -> **the mission overran its timer** (second symptom, same
cause). FIX (config-only): move the NIC to **0x340** in the DOSBox conf
(`nicbase=340`) AND the DOS `NET.CFG` (`PORT 340`) — both must agree; the
game uses odipkt so it's base-agnostic. VDB keeps 0x300 (game hardwires
it; real pod's NIC lived elsewhere for this reason). Non-networked gauge
runs never hit this (no NIC). Verified: heads render perfectly (radar with
contact blips + SPEED/HEADING/ARMOR + mission clock; MFDs full) AND the
mission ends cleanly on the timer (`Sending EndMission`, score 1000).
Diagnostic recipe: `New-Item <VPX_DUMPDIR>\DUMP` mid-mission dumps
win0/3/4.bmp; PrintWindow (flag 2, GL) the DOSBox SVGA window to prove the
framebuffer has content; framebuffer-full + heads-black => palette/port.
Committed: `net_full.conf`, `net_loop.conf`, `net-boot/` (drivers, NET.CFG
@340, loop.bat, README), `vpx-device/ne2000.cpp`, `vpx-device/ethernet_pcap.cpp`.
## Open questions / notes
- Exact TCP listen port(s) — not in the source grep; get from NETNUB.EXE
or a capture at milestone 3.
Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
Link Support
BUFFERS 6 1600
MEMPOOL 4096
Link Driver NE2000
PORT 340
INT 3
FRAME Ethernet_II
FRAME Ethernet_802.2
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
# net-boot — networked pod boot support files
Files the DOSBox pod mounts as `D:` when booting on the network path
(`emulator/net_full.conf`, `emulator/net_loop.conf`). The pod receives its
mission egg from the ops console (SheepShaver) over TCP 1501 — no `-egg` bypass.
## Contents
- `LSL.COM`, `NE2000.COM`, `ODIPKT.COM` — Novell ODI stack: Link Support Layer
→ NE2000 MLID → ODI-to-Packet-Driver shim. WATTCP/NetNub find the packet
driver by scanning INT 0x60-0x80 for `PKT DRVR` (odipkt lands at SINT 0x60).
- `NET.CFG` — ODI driver config. **`PORT 340`** (see below), `INT 3`,
`FRAME Ethernet_II`.
- `loop.bat` — GO.BAT-style loop: keeps relaunching `netnub -p -f btl4opt` so
the pod stays connected and picks up whatever mission the console queues.
## The 0x340 NIC base (important)
The VDB video splitter board is **hardwired at I/O 0x300-0x31A** (its palette
registers are at 0x300/0x308/0x310; `VDB_BASE` in `vpxlog.cpp`). If the NE2000
NIC is put at the usual `nicbase=300` it claims that same range and swallows
the game's mid-mission VDB palette writes → every MFD/radar head decodes to
**black**, and the VDB traffic corrupts the NIC's registers so the console's
EndMission is lost (the mission overruns its timer). The NIC is therefore at
**0x340** in both the DOSBox conf (`nicbase=340`) and here (`PORT 340`); they
must agree. The VDB keeps 0x300 (the game hardwires it — on the real pod the
NIC lived at a different base for exactly this reason).
## Bridge / pcap notes
See `../NET-NOTES.md` for the two-TAP Windows bridge, the bridge-miniport
`realnic` binding, and npcap DLL path requirements.
+9
View File
@@ -0,0 +1,9 @@
echo off
rem GO.BAT-style loop: keep relaunching netnub so the pod stays connected to
rem the console and picks up a mission whenever the operator queues one.
:go
32rtm.exe -x
netnub -p -f btl4opt > c:\rel410\bt\nn.log
32rtm.exe -u
echo ===== netnub cycle ended -- reconnecting to console =====
goto go
+69
View File
@@ -0,0 +1,69 @@
# Networked pod boot (single mission, then DOS pause).
# Console (SheepShaver) pushes the mission egg over TCP 1501; pod runs it.
# RIO on COM1 + dual AWE32 sound (host env VWE_AWE32=1 + VWE_AWE_ROM=<rom>).
#
# CRITICAL: NE2000 is at nicbase=340, NOT 300 -- the VDB video splitter board
# is hardwired at 0x300-0x31A (palettes 0x300/0x308/0x310). nicbase=300 makes
# the NIC swallow the game's VDB palette writes -> all MFD/radar heads decode
# to black AND corrupts NIC RX (console EndMission lost, mission overruns its
# timer). net-boot/NET.CFG must match (PORT 340). VDB stays at 0x300.
#
# Launch (host env): VPXLOG, VPX_RESPOND=1, VPX_RENDER=1, VPX_DUMPDIR,
# RIO_TAP, VWE_AWE32=1, VWE_AWE_ROM, and C:\Windows\System32\Npcap on PATH.
# pcap realnic must be the bridge miniport (see NET-NOTES.md).
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
nicirq=3
backend=pcap
[ethernet, pcap]
realnic=DB5521D
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[serial]
serial1=directserial realport:COM1 rxpollus:100 rxburst:16
serial2=disabled
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\bt
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s s g
echo === launching game via NetNub (RIO + sound; no -egg; waits for console) ===
32rtm.exe -x
netnub -p -f btl4opt > nn.log
32rtm.exe -u
echo === NET-RUN-DONE ===
pause
+43
View File
@@ -0,0 +1,43 @@
# Networked pod boot, GO.BAT-style loop (persistent: auto-relaunches netnub
# after each mission/idle disconnect so the pod stays connected to the console
# and picks up missions as the operator queues them -- mirrors the real pod's
# GO.BAT). Same 0x340 NIC fix as net_full.conf (see that file's header).
# Sound is OFF here (setenv arg3=n) for fast reconnect cycles; flip to net_full
# for the full sound experience.
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
nicirq=3
backend=pcap
[ethernet, pcap]
realnic=DB5521D
[serial]
serial1=directserial realport:COM1 rxpollus:100 rxburst:16
serial2=disabled
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\bt
set VIDEOFORMAT=svga
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r s n g
echo === entering GO.BAT-style netnub loop (waits for console missions) ===
call d:\loop.bat
+285
View File
@@ -0,0 +1,285 @@
/*
* Copyright (C) 2021 The DOSBox Team
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "config.h"
#if C_PCAP
#include "ethernet_pcap.h"
#include "dosbox.h"
#include "logging.h"
#include "support.h" /* strcasecmp */
#include <cstring>
extern std::string niclist;
#ifdef WIN32
#include <windows.h>
// DLL loading
#define pcap_sendpacket(A,B,C) PacketSendPacket(A,B,C)
#define pcap_close(A) PacketClose(A)
#define pcap_freealldevs(A) PacketFreealldevs(A)
#define pcap_open(A,B,C,D,E,F) PacketOpen(A,B,C,D,E,F)
#define pcap_next_ex(A,B,C) PacketNextEx(A,B,C)
#define pcap_findalldevs_ex(A,B,C,D) PacketFindALlDevsEx(A,B,C,D)
#define pcap_geterr(A) PacketGetError(A)
int (*PacketSendPacket)(pcap_t *, const u_char *, int) = 0;
void (*PacketClose)(pcap_t *) = 0;
void (*PacketFreealldevs)(pcap_if_t *) = 0;
pcap_t* (*PacketOpen)(char const *,int,int,int,struct pcap_rmtauth *,char *) = 0;
int (*PacketNextEx)(pcap_t *, struct pcap_pkthdr **, const u_char **) = 0;
int (*PacketFindALlDevsEx)(char *, struct pcap_rmtauth *, pcap_if_t **, char *) = 0;
char* (*PacketGetError)(pcap_t *) = nullptr;
char pcap_src_if_string[] = PCAP_SRC_IF_STRING;
bool LoadPcapLibrary() {
// remember if we've already initialized the library
static HINSTANCE pcapinst = (HINSTANCE)-1;
if(pcapinst!=(HINSTANCE)-1) {
return (pcapinst!=NULL);
}
// init the library
pcapinst = LoadLibrary("WPCAP.DLL");
if(pcapinst==NULL) {
niclist = "WinPcap has to be installed for the NE2000 to work.";
LOG_MSG(niclist.c_str());
return false;
}
FARPROC psp;
#ifdef __MINGW32__
// C++ defines function and data pointers as separate types to reflect
// Harvard architecture machines (like the Arduino). As such, casting
// between them isn't portable and GCC will helpfully warn us about it.
// We're only running this code on Windows which explicitly allows this
// behaviour, so silence the warning to avoid confusion.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
psp = GetProcAddress(pcapinst,"pcap_sendpacket");
if(!PacketSendPacket) PacketSendPacket =
(int (__cdecl *)(pcap_t *,const u_char *,int))psp;
psp = GetProcAddress(pcapinst,"pcap_close");
if(!PacketClose) PacketClose =
(void (__cdecl *)(pcap_t *)) psp;
psp = GetProcAddress(pcapinst,"pcap_freealldevs");
if(!PacketFreealldevs) PacketFreealldevs =
(void (__cdecl *)(pcap_if_t *)) psp;
psp = GetProcAddress(pcapinst,"pcap_open");
if(!PacketOpen) PacketOpen =
(pcap_t* (__cdecl *)(char const *,int,int,int,struct pcap_rmtauth *,char *)) psp;
psp = GetProcAddress(pcapinst,"pcap_next_ex");
if(!PacketNextEx) PacketNextEx =
(int (__cdecl *)(pcap_t *, struct pcap_pkthdr **, const u_char **)) psp;
psp = GetProcAddress(pcapinst,"pcap_findalldevs_ex");
if(!PacketFindALlDevsEx) PacketFindALlDevsEx =
(int (__cdecl *)(char *, struct pcap_rmtauth *, pcap_if_t **, char *)) psp;
psp = GetProcAddress(pcapinst,"pcap_geterr");
if(!PacketGetError) PacketGetError =
(char* (__cdecl *)(pcap_t *)) psp;
#ifdef __MINGW32__
#pragma GCC diagnostic pop
#endif
if(PacketFindALlDevsEx==0 || PacketNextEx==0 || PacketOpen==0 ||
PacketFreealldevs==0 || PacketClose==0 || PacketSendPacket==0 ||
PacketGetError==0) {
niclist = "Incorrect or non-functional WinPcap version.";
LOG_MSG(niclist.c_str());
pcapinst = NULL;
return false;
}
return true;
}
#endif
PcapEthernetConnection::PcapEthernetConnection()
: EthernetConnection()
{
}
PcapEthernetConnection::~PcapEthernetConnection()
{
if(adhandle) pcap_close(adhandle);
}
bool PcapEthernetConnection::Initialize(Section* config)
{
Section_prop *section = static_cast<Section_prop*>(config);
const char* realnicstring = section->Get_string("realnic");
#ifdef WIN32
if(!LoadPcapLibrary()) {
return false;
}
#endif
// find out which pcap device to use
pcap_if_t *alldevs;
pcap_if_t *currentdev = NULL;
char errbuf[PCAP_ERRBUF_SIZE];
unsigned int userdev;
#ifdef WIN32
if (pcap_findalldevs_ex(pcap_src_if_string, NULL, &alldevs, errbuf) == -1)
#else
if (pcap_findalldevs(&alldevs, errbuf) == -1)
#endif
{
niclist = "Cannot enumerate network interfaces: "+std::string(errbuf);
LOG_MSG("%s", niclist.c_str());
return false;
}
{
Bitu i = 0;
niclist = "Network Interface List\n-------------------------------------------------------------\n";
for(currentdev=alldevs; currentdev!=NULL; currentdev=currentdev->next) {
const char* desc = "no description";
if(currentdev->description) desc=currentdev->description;
i++;
niclist+=(i<10?" ":"")+std::to_string(i)+" "+currentdev->name+"\n ("+desc+")\n";
}
}
if (!strcasecmp(realnicstring,"list")) {
// print list and quit
std::istringstream in(("\n"+niclist+"\n").c_str());
if (in) for (std::string line; std::getline(in, line); )
LOG_MSG("%s", line.c_str());
LOG_MSG("NOTE: No interface was chosen because the realnic setting is set to 'list', set realnic to the number of the interface you want to use.");
pcap_freealldevs(alldevs);
return false;
} else if(1==sscanf(realnicstring,"%u",&userdev)) {
// user passed us a number
Bitu i = 0;
currentdev=alldevs;
while(currentdev!=NULL) {
i++;
if(i==userdev) break;
else currentdev=currentdev->next;
}
} else {
// user might have passed a piece of name
for(currentdev=alldevs; currentdev!=NULL; currentdev=currentdev->next) {
if(strstr(currentdev->name,realnicstring)) {
break;
}else if(currentdev->description!=NULL &&
strstr(currentdev->description,realnicstring)) {
break;
}
}
}
if(currentdev==NULL) {
LOG_MSG("Unable to find network interface - check realnic parameter\n");
pcap_freealldevs(alldevs);
return false;
}
// print out which interface we are going to use
const char* desc = "no description";
if(currentdev->description) desc=currentdev->description;
LOG_MSG("Using Network interface:\n%s\n(%s)\n",currentdev->name,desc);
const char *timeoutstr = section->Get_string("timeout");
char *end;
int timeout = -1;
if (!strlen(timeoutstr)||(timeoutstr[0]!='-'&&!isdigit(timeoutstr[0]))) { // Default timeout values
#ifdef MACOSX
timeout = 3000; // For macOS, use 3000ms as it does not appear to support -1
#else
timeout = -1; // For other platforms, use -1 which should mean "non-blocking mode"
#endif
} else
timeout = strtoul(timeoutstr,&end,10);
// attempt to open it
#ifdef WIN32
if ( (adhandle= pcap_open(
currentdev->name, // name of the device
65536, // portion of the packet to capture
// 65536 = whole packet
PCAP_OPENFLAG_PROMISCUOUS, // promiscuous mode
timeout, // read timeout
NULL, // authentication on the remote machine
errbuf // error buffer
) ) == NULL)
#else
/*pcap_t *pcap_open_live(const char *device, int snaplen,
int promisc, int to_ms, char *errbuf)*/
if ( (adhandle= pcap_open_live(
currentdev->name, // name of the device
65536, // portion of the packet to capture
// 65536 = whole packet
true, // promiscuous mode
timeout, // read timeout
errbuf // error buffer
) ) == NULL)
#endif
{
LOG_MSG("\nUnable to open the interface: %s.", errbuf);
pcap_freealldevs(alldevs);
return false;
}
pcap_freealldevs(alldevs);
#ifndef WIN32
pcap_setnonblock(adhandle,1,errbuf);
#endif
return true;
}
void PcapEthernetConnection::SendPacket(const uint8_t* packet, int len)
{
static unsigned long txn = 0;
if (((++txn) & (txn - 1)) == 0) /* powers of 2, avoid spam */
LOG_MSG("PCAP TX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x",
txn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8]);
int ret = pcap_sendpacket(adhandle, packet, len);
if(ret) LOG_MSG("PCAP error: %s", pcap_geterr(adhandle));
}
void PcapEthernetConnection::GetPackets(std::function<void(const uint8_t*, int)> callback)
{
int res;
struct pcap_pkthdr *header;
u_char *pkt_data;
static unsigned long rxn = 0;
//#if 0
while((res = pcap_next_ex( adhandle, &header, (const u_char **)&pkt_data)) > 0) {
if (((++rxn) & (rxn - 1)) == 0)
LOG_MSG("PCAP RX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x",
rxn, header->len, pkt_data[0],pkt_data[1],pkt_data[2],
pkt_data[6],pkt_data[7],pkt_data[8]);
callback(pkt_data, header->len);
}
//#endif
}
#endif
File diff suppressed because it is too large Load Diff