- ra3.webgl: a Web peer of Vulkan/Direct3D (SDL3 canvas + GLES3/WebGL2), with 2D blit and the GPU terrain raymarch; GLSL ES shaders; null fallback off Emscripten. backend::webgl + default_backend() (webgl under Emscripten). - Emscripten toolchain + Dockerfile.wasm + scripts/build-wasm.sh; import std, global -fexceptions, and Asyncify so the blocking frame loop yields to the browser (display::sleep_frame calls emscripten_sleep). - apps/web/shell.html + apps/web/serve.py (Range-capable dev server). - Assets: browsers forbid synchronous on-demand reads on the main thread (and FS.createLazyFile / the WasmFS fetch backend are worker-only; SDL3's Emscripten backend is main-thread DOM only, so the engine cannot run in a worker). The wasm build therefore preloads a compact per-map set via OPENRA3_WEB_ASSETS. - New openra3 textures --map ID lists the loose terrain TGAs a map resolves to (terrain::resolve_texture_files, shared with load_textures_from_dir). - Fixes: webgl heightmap used GL_R16 = 0x8229 (that is R8) -> upload rejected, terrain flattened to water; now 0x822A. Logger uses a stdout console sink on the web (stderr maps to console.error); a GL error check logs bad uploads. - CI: wasm image + build jobs.
108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Static file server with HTTP Range support, for the OpenRA3 wasm build.
|
|
|
|
Emscripten's lazy MEMFS reads assets back as byte ranges, so the server must
|
|
honour the ``Range`` header -- Python's stdlib ``http.server`` does not.
|
|
|
|
python3 apps/web/serve.py --root build/wasm/bin --port 8199
|
|
# then open http://localhost:8199/openra3.html
|
|
|
|
Serve ``--root`` containing ``openra3.html/.js/.wasm``, ``assets.manifest.json``
|
|
and the extracted ``assets/`` tree (a directory or a junction/symlink to it).
|
|
"""
|
|
|
|
import argparse
|
|
import functools
|
|
import os
|
|
import re
|
|
import sys
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
_RANGE = re.compile(r"bytes=(\d*)-(\d*)$")
|
|
|
|
|
|
class RangeHandler(SimpleHTTPRequestHandler):
|
|
# SharedArrayBuffer (needed by the pthreads build) requires cross-origin
|
|
# isolation.
|
|
def end_headers(self):
|
|
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
|
|
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
|
|
super().end_headers()
|
|
|
|
def send_head(self):
|
|
header = self.headers.get("Range")
|
|
if not header:
|
|
return super().send_head()
|
|
|
|
path = self.translate_path(self.path)
|
|
if os.path.isdir(path):
|
|
return super().send_head()
|
|
try:
|
|
handle = open(path, "rb")
|
|
except OSError:
|
|
self.send_error(404, "File not found")
|
|
return None
|
|
|
|
match = _RANGE.match(header.strip())
|
|
if not match:
|
|
handle.close()
|
|
return super().send_head()
|
|
|
|
size = os.fstat(handle.fileno()).st_size
|
|
start = int(match.group(1)) if match.group(1) else 0
|
|
end = int(match.group(2)) if match.group(2) else size - 1
|
|
if start > end or start >= size:
|
|
handle.close()
|
|
self.send_error(416, "Requested Range Not Satisfiable")
|
|
return None
|
|
end = min(end, size - 1)
|
|
|
|
self.send_response(206)
|
|
self.send_header("Content-Type", self.guess_type(path))
|
|
self.send_header("Accept-Ranges", "bytes")
|
|
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
|
|
self.send_header("Content-Length", str(end - start + 1))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.end_headers()
|
|
handle.seek(start)
|
|
self._remaining = end - start + 1
|
|
return handle
|
|
|
|
def copyfile(self, source, outputfile):
|
|
remaining = getattr(self, "_remaining", None)
|
|
if remaining is None:
|
|
return super().copyfile(source, outputfile)
|
|
try:
|
|
while remaining > 0:
|
|
chunk = source.read(min(65536, remaining))
|
|
if not chunk:
|
|
break
|
|
outputfile.write(chunk)
|
|
remaining -= len(chunk)
|
|
finally:
|
|
self._remaining = None
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="OpenRA3 wasm dev server")
|
|
parser.add_argument("--root", default="build/wasm/bin", help="directory to serve")
|
|
parser.add_argument("--port", type=int, default=8199)
|
|
parser.add_argument("--bind", default="127.0.0.1")
|
|
args = parser.parse_args()
|
|
|
|
if not os.path.isdir(args.root):
|
|
print(f"root not found: {args.root}", file=sys.stderr)
|
|
return 1
|
|
handler = functools.partial(RangeHandler, directory=args.root)
|
|
server = ThreadingHTTPServer((args.bind, args.port), handler)
|
|
print(f"serving {args.root} at http://{args.bind}:{args.port}/openra3.html")
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|