RP experiment finding: RP runs on the BT pod emulator (shared MUNGA stack) but
its cockpit VDB heads mis-decode. Root causes captured live (VDB_PALDUMP +
vpxresp log): (1) framebuffer identical to BT (mode 0x111, 640x480); (2) RP
loads pal0 (BT's dynamic color-radar palette) as ALL ZEROS -> radar head black;
(3) RP actively drives the VGA-DAC pixel-mask register that BT leaves at 0xFF,
and pal_draw ignored it -> likely the MFD garble.
Device change: pixel-mask writes now log their VALUE on change
("# VDB pixel-mask[gN] = 0xXX"); pal_draw ANDs the palette index with the
group mask under VDB_APPLYMASK=1 (default off = BT-identical); masks default to
0xFF in vdb_reset so an un-driven group still decodes. Built clean under MINGW64.
Also: Mac Console 4.10 retired from regular use (host .NET console is the daily
driver); tap2_mirror.py PARKED with revive notes for console A/B only. The
SendToRxAdapters registry stays set (serves the .NET host console).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
6.4 KiB
Python
140 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""[PARKED 2026-07-10] The SheepShaver/Mac Console 4.10 is retired from
|
|
regular use -- the modern host-side .NET TeslaConsole is the daily driver
|
|
(it talks to the pod directly via the bridge IP + SendToRxAdapters; no
|
|
mirror needed). Keep this tool for the ONE case it exists for: running the
|
|
1996 Mac console ALONGSIDE the .NET console for A/B testing. To revive: start
|
|
a pod, then `pythonw net-tools/tap2_mirror.py`, and bring up SheepShaver +
|
|
Console 4.10 (see NET-NOTES.md "BOTH CONSOLES AT ONCE"). Verified working
|
|
2026-07-09 (Console 4.10 ran a full mission through it). Constants at top
|
|
(pod NE2000 MAC, TAP2 GUID) may need refreshing if the adapters change.
|
|
----------------------------------------------------------------------------
|
|
Pod->Mac mirror for the SendToRx era: with Npcap's SendToRxAdapters set
|
|
on the Network Bridge (so the host .NET console hears the pod), the pod's
|
|
injected frames no longer reach the TAP members -- SheepShaver goes deaf.
|
|
Every other path still works. This daemon closes the one missing hop:
|
|
capture pod-sourced frames destined for the Mac (or broadcast) on the
|
|
bridge, and write them into TAP2's free application side; the Windows
|
|
bridge then forwards them member->member to TAP1 where SheepShaver reads
|
|
them. Result: BOTH consoles (Mac 4.10 + host .NET) work at once.
|
|
|
|
A short-lived frame-hash dedup breaks the reinjection loop (the TAP2 copy
|
|
is re-captured on the bridge; identical bytes within the window = skip).
|
|
|
|
SECOND DIRECTION (required!): writing pod-sourced frames into TAP2 teaches
|
|
the Windows bridge that the pod's MAC lives on TAP2 -- from then on the
|
|
Mac's UNICAST frames to the pod are steered to TAP2's app side instead of
|
|
up the miniport where the pod's pcap listens. So a reader thread drains
|
|
TAP2 and re-injects those frames on the bridge (under SendToRxAdapters the
|
|
injection is RX-indicated up, exactly where the pod hears it). Frames the
|
|
pod itself sourced are never re-injected (loop guard).
|
|
|
|
Run with any CPython (ctypes only). Stop with Ctrl+C / kill.
|
|
"""
|
|
import ctypes, os, struct, sys, threading, time, zlib
|
|
from ctypes import wintypes as wt
|
|
|
|
BRIDGE = rb"\Device\NPF_{5DB5521D-2D56-40E8-9E3D-3B36C9EE7C8F}"
|
|
TAP2_PATH = r"\\.\Global\{C78EA2D1-6120-4944-A949-786B1B963CB5}.tap"
|
|
POD_MAC = bytes.fromhex("ce3d72673869") # DOSBox NE2000 (stable across runs)
|
|
MAC_MAC = bytes.fromhex("525400e73ef0") # SheepShaver (etherpermanentaddress)
|
|
BCAST = b"\xff" * 6
|
|
|
|
os.add_dll_directory(r"C:\Windows\System32\Npcap")
|
|
pc = ctypes.WinDLL(r"C:\Windows\System32\Npcap\wpcap.dll")
|
|
pc.pcap_open_live.restype = ctypes.c_void_p
|
|
pc.pcap_open_live.argtypes = [ctypes.c_char_p] + [ctypes.c_int]*3 + [ctypes.c_char_p]
|
|
pc.pcap_next_ex.restype = ctypes.c_int
|
|
pc.pcap_next_ex.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p),
|
|
ctypes.POINTER(ctypes.c_void_p)]
|
|
pc.pcap_setnonblock.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p]
|
|
|
|
class PktHdr(ctypes.Structure):
|
|
_fields_ = [("s", ctypes.c_long), ("u", ctypes.c_long),
|
|
("cl", ctypes.c_uint), ("l", ctypes.c_uint)]
|
|
|
|
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
GENERIC_RW = 0xC0000000
|
|
OPEN_EXISTING = 3
|
|
FILE_ATTRIBUTE_SYSTEM = 0x4
|
|
tap = k32.CreateFileW(TAP2_PATH, GENERIC_RW, 0, None, OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_SYSTEM, None)
|
|
if tap == wt.HANDLE(-1).value:
|
|
sys.exit(f"cannot open TAP2 app side ({ctypes.get_last_error()}) -- is "
|
|
"another process attached?")
|
|
# TAP_IOCTL_SET_MEDIA_STATUS(1): belt & braces alongside the registry force
|
|
TAP_IOCTL_SET_MEDIA_STATUS = 0x22C018
|
|
status = ctypes.c_ulong(1)
|
|
ret = ctypes.c_ulong(0)
|
|
k32.DeviceIoControl(tap, TAP_IOCTL_SET_MEDIA_STATUS, ctypes.byref(status), 4,
|
|
ctypes.byref(status), 4, ctypes.byref(ret), None)
|
|
|
|
err = ctypes.create_string_buffer(256)
|
|
cap = pc.pcap_open_live(BRIDGE, 65535, 1, 20, err)
|
|
if not cap:
|
|
sys.exit(f"pcap open failed: {err.value.decode()}")
|
|
pc.pcap_setnonblock(cap, 1, err)
|
|
pc.pcap_sendpacket.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
|
|
|
|
# a second pcap handle for the reader thread's re-injections (thread safety)
|
|
inj = pc.pcap_open_live(BRIDGE, 65535, 0, 20, err)
|
|
if not inj:
|
|
sys.exit(f"pcap inject-handle open failed: {err.value.decode()}")
|
|
|
|
reinjected = [0]
|
|
def tap_reader():
|
|
"""Drain frames the bridge steers INTO TAP2 (unicasts to MACs it has
|
|
learned on this port -- i.e. the Mac's frames to the pod once the
|
|
mirror has been writing) and re-inject them on the bridge so the
|
|
pod's capture receives them. Never re-inject pod-sourced frames."""
|
|
buf = ctypes.create_string_buffer(2048)
|
|
n = ctypes.c_ulong(0)
|
|
while True:
|
|
ok = k32.ReadFile(tap, buf, 2048, ctypes.byref(n), None)
|
|
if not ok or n.value < 14:
|
|
time.sleep(0.002)
|
|
continue
|
|
frame = buf.raw[:n.value]
|
|
if frame[6:12] == POD_MAC:
|
|
continue
|
|
pc.pcap_sendpacket(inj, frame, len(frame))
|
|
reinjected[0] += 1
|
|
|
|
threading.Thread(target=tap_reader, daemon=True).start()
|
|
|
|
print(f"pod->Mac mirror up: bridge capture -> TAP2 app side "
|
|
f"(pod {POD_MAC.hex(':')}, mac {MAC_MAC.hex(':')}); "
|
|
f"TAP2 reader re-injects bridge-steered frames", flush=True)
|
|
|
|
recent = {} # frame crc32 -> expiry time (loop breaker)
|
|
hdr = ctypes.c_void_p(); dat = ctypes.c_void_p()
|
|
mirrored = dropped_dup = 0
|
|
last_report = time.time()
|
|
while True:
|
|
r = pc.pcap_next_ex(cap, ctypes.byref(hdr), ctypes.byref(dat))
|
|
now = time.time()
|
|
if r == 1:
|
|
ph = ctypes.cast(hdr, ctypes.POINTER(PktHdr)).contents
|
|
if ph.cl >= 14:
|
|
frame = ctypes.string_at(dat, ph.cl)
|
|
if frame[6:12] == POD_MAC and (frame[0:6] == MAC_MAC
|
|
or frame[0:6] == BCAST):
|
|
h = zlib.crc32(frame)
|
|
exp = recent.get(h)
|
|
if exp is not None and exp > now:
|
|
dropped_dup += 1
|
|
else:
|
|
recent[h] = now + 0.2
|
|
written = ctypes.c_ulong(0)
|
|
k32.WriteFile(tap, frame, len(frame),
|
|
ctypes.byref(written), None)
|
|
mirrored += 1
|
|
else:
|
|
time.sleep(0.005)
|
|
if now - last_report > 30:
|
|
last_report = now
|
|
for h in [k for k, v in recent.items() if v <= now]:
|
|
del recent[h]
|
|
print(f"mirrored={mirrored} dup_dropped={dropped_dup} "
|
|
f"reinjected={reinjected[0]}", flush=True)
|