- 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
87 lines
4.3 KiB
Python
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")
|