EBLAN_ENGINE/tools/test_preview_ipc.py
Artvasil09 a1d1baa869 feat: complete engine and editor subsystems (Stages 1-5 from TODO_ARTEM)
- Stage 1: Eliminate fish-eye distortion with precomputed tangential ray offset table and fractional DDA depth
- Stage 2: Full asset management in EblanStudio (stb_image/stb_image_write, 128x128 BGRA texture packaging, WAV sound manager)
- Stage 3: Multi-level room streaming, LevelTransition/CELL_PORTAL trigger system, 16-frame screen fade to black
- Stage 4: 60Hz cutscene engine in pure x86 ASM (v4 binary track, waypoint camera motion, shortest-arc pan, CP1251 Russian subtitles, letterbox overlay, audio)
- Stage 5: EblanStudio cutscene timeline (F6) and windowless Live ASM Viewport over shared-memory IPC directly rendered into DX11
- Tooling & Tests: comprehensive regression test suite (94 editor + 107 engine tests), IPC tests, and cutscene demo
2026-10-22 16:24:22 +03:00

87 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Exercise the actual windowless ASM renderer through its public mailbox."""
import ctypes as C
import mmap
import os
from pathlib import Path
import struct
import subprocess
import time
ROOT = Path(__file__).resolve().parents[1]
SIZE, TEXTURES, PIXELS = 1357792, 2016, 1050592
def check_preview(engine, expected=None, output=None):
name = f"Local\\EblanPreview-test-{os.getpid()}-{time.monotonic_ns()}"
with mmap.mmap(-1, SIZE, tagname=name) as packet:
base = C.addressof(C.c_char.from_buffer(packet))
state = C.cast(base+8, C.POINTER(C.c_long))
struct.pack_into("<4I3i6I", packet, 0, 0x56504245, 1, 0, os.getpid(), 160, 1024, 0,
0x383838, 0x222222, 0, 0, 0, 1)
for y in range(32): packet[128+y*32+10] = 1
# Flat red wall, other slots black; proves texture upload rather than fallback.
packet[TEXTURES+65536:TEXTURES+2*65536] = struct.pack("<I",0xE02010)*16384
proc = subprocess.Popen([str(engine), "--preview:"+name], cwd=ROOT,
creationflags=subprocess.CREATE_NO_WINDOW)
def request():
# Aligned DWORD stores are atomic and release-ordered on x86/x64.
# Payload writes above finish before publishing the request.
state[0]=1
deadline = time.monotonic()+5
while state[0] != 2:
if proc.poll() is not None: raise AssertionError(f"Preview process exited: {proc.returncode}")
if time.monotonic()>deadline: raise AssertionError("Preview renderer did not answer")
time.sleep(.005)
return packet[PIXELS:PIXELS+307200]
try:
frame=request()
middle=struct.unpack_from("<I",frame,(120*320+160)*4)[0]
assert middle not in (0,0x383838,0x222222), "Wall was not rendered"
packet[128+16*32+5]=4
changed=request()
assert changed!=frame, "Unsaved wall change did not reach the renderer"
packet[128+16*32+5]=0
struct.pack_into("<2I",packet,40,1,16)
assert not any(request()), "Full cinematic fade is not black"
struct.pack_into("<2I",packet,40,1,0)
packet[1920:1920+7]="Привет".encode("cp1251")+b"\0"
subtitle=request()
assert not any(subtitle[:8*320*4]) and not any(subtitle[188*320*4:]), "Letterbox missing"
assert subtitle!=frame, "Subtitle overlay missing"
# Compare the live IPC output with a direct Render3DScene regression dump.
if expected:
data=(ROOT/"assets/map.dat").read_bytes()
h=struct.unpack_from("<I7HII",data)
version=h[1]; count=h[7]
struct.pack_into("<3i",packet,16,h[4]*64+32,h[5]*64+32,h[6])
struct.pack_into("<5I",packet,28,h[8],h[9],count,0,0)
packet[128:1152]=data[38:1062]
sprites=bytearray(data[1062:1062+count*24])
if version==1:
for i in range(count):
x,y=struct.unpack_from("<ii",sprites,i*24)
struct.pack_into("<ii",sprites,i*24,x>>16,y>>16)
packet[1152:1152+len(sprites)]=sprites
pack=(ROOT/"assets/textures/texturepack.bin").read_bytes()
packet[TEXTURES:TEXTURES+len(pack)-8]=pack[8:]
struct.pack_into("<I",packet,48,2)
actual=request()
assert actual==Path(expected).read_bytes(), "Viewport differs from the standalone ASM renderer"
if output:
header=struct.pack("<2sIHHI",b"BM",307254,0,0,54)
header+=struct.pack("<IiiHHIIiiII",40,320,-240,1,32,0,307200,0,0,0,0)
Path(output).write_bytes(header+actual)
state[0]=3
assert proc.wait(timeout=5)==0, "Preview did not exit cleanly"
print("PASS live ASM viewport: map edits, texture upload, letterbox, subtitles, fade and clean shutdown")
if expected: print("PASS live viewport pixels exactly match the standalone ASM renderer")
finally:
if proc.poll() is None: proc.terminate(); proc.wait(timeout=5)
del state
if __name__ == "__main__":
check_preview(ROOT/"engine.exe")