mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-19 01:00:27 +02:00
The arm64 and Android guests are compiled for arm64_32-apple-watchos and
their Darwin assembly converted to ELF (tools/android_asm_convert.py). The
converter took a quoted .ascii/.asciz operand for a quoted symbol name and
dropped a leading "_" from it, as it does from symbols; and its quoted-name
pattern ended at an escaped quote (\"), so the "_" of a word after one was
dropped too. Clang writes byte tables as .ascii strings, so a table holding
95 ("_") lost that byte and everything after it moved down by one.
In Opus that table was CELT's pulse cache (modes.c, cache_bits50): the bits
a band may have are read off it, and shifted by a byte it lets a band have
more pulses than the PVQ tables hold. Decoding a frame from another player
then read past CELT_PVQ_U_ROW in decode_pulses (n 32, k 240) and crashed: a
client on the Steam Frame, in a CTF game on the internet. The guest's own
encoder could crash likewise in encode_pulses. The same build of Opus,
driven with the game's settings and with clean, corrupt, truncated and
random packets and voice_audio_play's loss handling, crashed within a few
thousand frames (and identically at -O0); with the fix it ran 4.8 million
decodes clean, and its encoded bytes and decoded samples are identical to a
native 64-bit build's.
The same loss hit other strings and tables of the guest: musl's iswalpha,
iswpunct and towctrans tables and iconv's charmaps; tomlc17's "_0123..."
digit sets (TOML numbers with "_"); the attract mode's "_de"/"_fr" names,
"_lan_", "_lit" and "_%s"; the progress bar's compressed monk tone and the
DirectSound effects image; and assertion and error texts. All are now
assembled as compiled.
String directives now go through unchanged, and quoted names honour
escapes. tools/test_android_asm_convert.py checks both, and a table of
every byte value through clang, the conversion and the assembler.
258 lines
11 KiB
Python
258 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Embeds the high-res HUD textures (port/assets/hud, made by
|
|
tools/hud_assets.py), the menus' titles (port/assets/titles, made by
|
|
tools/title_assets.py), their controller button icons (port/assets/buttons,
|
|
made by tools/button_assets.py), the fonts the text is drawn with
|
|
(port/assets/fonts), the menus' files (port/assets/menus, made by
|
|
tools/ce_menus.py) and SMAA's shader and lookup textures
|
|
(port/third_party/smaa) in the game as C data:
|
|
|
|
python tools/embed_assets.py OUTPUT.c
|
|
|
|
writes OUTPUT.c with each PNG and the bitmap it stands for (its tag, index
|
|
and checksum of its pixels, from port/assets/hud/layout.json,
|
|
port/assets/titles/titles.json and port/assets/buttons/buttons.json), the
|
|
fonts and their tags from port/assets/fonts/fonts.json, the menu files
|
|
listed in port/assets/menus/menus.json, and SMAA's shader and lookup
|
|
textures, as port/linux/src/hud_hires.h, text_hires.h, menu_files.h and
|
|
xgpu_post.c declare them. The builds generate it
|
|
(hud_assets_build, called by tools/linux_build.py, windows_build.py and
|
|
android_build.py), so the assets are the committed source and Android needs
|
|
no files beside its guest image.
|
|
|
|
The data are 32-bit words, not bytes: the Android build passes the guest's
|
|
assembly through tools/android_asm_convert.py, which rewrites identifiers in
|
|
operands (it keeps .ascii strings as they are, but once garbled them).
|
|
"""
|
|
|
|
import json
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, List
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
HUD_ASSETS = Path("port/assets/hud")
|
|
LAYOUT = HUD_ASSETS / "layout.json"
|
|
TITLE_ASSETS = Path("port/assets/titles")
|
|
TITLE_LIST = TITLE_ASSETS / "titles.json"
|
|
BUTTON_ASSETS = Path("port/assets/buttons")
|
|
BUTTON_LIST = BUTTON_ASSETS / "buttons.json"
|
|
FONT_ASSETS = Path("port/assets/fonts")
|
|
FONT_LIST = FONT_ASSETS / "fonts.json"
|
|
MENU_ASSETS = Path("port/assets/menus")
|
|
MENU_LIST = MENU_ASSETS / "menus.json"
|
|
# SMAA's files and the names port/linux/src/xgpu_post.c declares them by
|
|
SMAA_ASSETS = Path("port/third_party/smaa")
|
|
SMAA_FILES = (("SMAA.hlsl", "xgpu_smaa_shader"), ("area_tex.zlib", "xgpu_smaa_area_texture"),
|
|
("search_tex.zlib", "xgpu_smaa_search_texture"))
|
|
# the desktop windows' icon (tools/app_icon.py; port/linux/src/sdl_platform.c)
|
|
WINDOW_ICON = Path("port/assets/icon/opence-icon-256.png")
|
|
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
|
|
|
|
def font_files() -> List[str]:
|
|
"""The font files fonts.json uses, each once."""
|
|
if not (ROOT / FONT_LIST).is_file():
|
|
return []
|
|
fonts = json.loads((ROOT / FONT_LIST).read_text())["fonts"]
|
|
return sorted({font["file"] for font in fonts})
|
|
|
|
|
|
def textures() -> List[tuple]:
|
|
"""The textures: each one's folder, its entry in its list, and whether
|
|
it is the menus' (a title or a button icon, drawn with the high-res
|
|
text)."""
|
|
result = []
|
|
for folder, listing, title in ((HUD_ASSETS, LAYOUT, False), (TITLE_ASSETS, TITLE_LIST, True),
|
|
(BUTTON_ASSETS, BUTTON_LIST, True)):
|
|
if (ROOT / listing).is_file():
|
|
result += [(folder, asset, title) for asset in json.loads((ROOT / listing).read_text())["assets"]]
|
|
return result
|
|
|
|
|
|
def menu_files() -> List[str]:
|
|
"""The menus' files menus.json lists, relative to their folder."""
|
|
if not (ROOT / MENU_LIST).is_file():
|
|
return []
|
|
return json.loads((ROOT / MENU_LIST).read_text())["files"]
|
|
|
|
|
|
def smaa_files() -> List[tuple]:
|
|
"""SMAA's files that the checkout has, with their symbols."""
|
|
return [(name, symbol) for name, symbol in SMAA_FILES if (ROOT / SMAA_ASSETS / name).is_file()]
|
|
|
|
|
|
def hud_asset_inputs() -> List[Path]:
|
|
"""The files the generated source is made from."""
|
|
inputs = [listing for listing in (LAYOUT, TITLE_LIST, BUTTON_LIST, FONT_LIST, MENU_LIST)
|
|
if (ROOT / listing).is_file()]
|
|
return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()),
|
|
*(FONT_ASSETS / name for name in font_files()), *(MENU_ASSETS / name for name in menu_files()),
|
|
*(SMAA_ASSETS / name for name, _ in smaa_files()),
|
|
*([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else [])]
|
|
|
|
|
|
def hud_configure_inputs() -> List[Path]:
|
|
"""What configure.py is run again for: the lists of assets (and the
|
|
folders, for files added or removed), not each file, which a change of a
|
|
list may rename or remove."""
|
|
inputs = []
|
|
for folder, listing in ((HUD_ASSETS, LAYOUT), (TITLE_ASSETS, TITLE_LIST), (BUTTON_ASSETS, BUTTON_LIST),
|
|
(FONT_ASSETS, FONT_LIST), (MENU_ASSETS, MENU_LIST)):
|
|
if (ROOT / listing).is_file():
|
|
inputs += [folder, listing]
|
|
if (ROOT / SMAA_ASSETS).is_dir():
|
|
inputs.append(SMAA_ASSETS)
|
|
return inputs
|
|
|
|
|
|
def words(data: bytes) -> List[str]:
|
|
"""data as lines of 32-bit words (padded with zeros)."""
|
|
padded = data + b"\0" * (-len(data) % 4)
|
|
values = struct.unpack(f"<{len(padded) // 4}I", padded)
|
|
return ["\t" + ", ".join(f"0x{word:08x}" for word in values[start:start + 8]) + ","
|
|
for start in range(0, len(values), 8)]
|
|
|
|
|
|
def hud_assets_build(n: Any, prefix: str, output: Path) -> List[Path]:
|
|
"""Emits the rule that generates output; returns [output]. It is made
|
|
whatever assets the checkout has (with none, its tables are empty), so
|
|
that the symbols the platform layer refers to are always defined."""
|
|
inputs = hud_asset_inputs()
|
|
n.rule(
|
|
name=f"{prefix}_embed_assets",
|
|
command="$python tools/embed_assets.py $out",
|
|
description=f"{prefix.upper()} EMBED $out",
|
|
)
|
|
n.build(outputs=output, rule=f"{prefix}_embed_assets", implicit=[Path("tools/embed_assets.py"), *inputs])
|
|
return [output]
|
|
|
|
|
|
def png_size(data: bytes, name: str) -> tuple:
|
|
"""The width and height of an 8-bit RGBA, non-interlaced PNG (the only
|
|
kind port/linux/src/hud_hires.c reads)."""
|
|
if data[:8] != PNG_SIGNATURE or data[12:16] != b"IHDR":
|
|
sys.exit(f"{name}: not a PNG")
|
|
width, height, depth, colour, _, _, interlace = struct.unpack(">IIBBBBB", data[16:29])
|
|
if depth != 8 or colour != 6 or interlace != 0:
|
|
sys.exit(f"{name}: must be 8-bit RGBA and not interlaced")
|
|
return width, height
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 2:
|
|
sys.exit("usage: embed_assets.py OUTPUT.c")
|
|
lines = [
|
|
"/* generated by tools/embed_assets.py from port/assets: do not edit */",
|
|
"",
|
|
'#include "hud_hires.h"',
|
|
"",
|
|
]
|
|
table = []
|
|
for index, (folder, asset, title) in enumerate(textures()):
|
|
name = f"{asset['name']}.png"
|
|
data = (ROOT / folder / name).read_bytes()
|
|
width, height = png_size(data, name)
|
|
scale = asset["scale"]
|
|
if (width, height) != (asset["width"] * scale, asset["height"] * scale):
|
|
sys.exit(f"{name}: {width}x{height}, not {scale}x its bitmap's {asset['width']}x{asset['height']}")
|
|
lines.append(f"static const unsigned int asset{index}[] = {{")
|
|
lines.extend(words(data))
|
|
lines.append("};")
|
|
lines.append("")
|
|
tag = asset["tag"].replace("\\", "\\\\")
|
|
coverage = int(any(cell["kind"] == "meter" for cell in asset.get("cells", [])))
|
|
point_threshold = int(any(cell.get("thresholds") for cell in asset.get("cells", [])))
|
|
# (the sequences whose sprites it stands for, a bit each of 32; 0: the whole bitmap)
|
|
sequences = [sprite["sequence"] for sprite in asset.get("sprites", [])]
|
|
if any(not 0 <= sequence < 32 for sequence in sequences):
|
|
sys.exit(f"{name}: sprite sequences must be 0 to 31 (hud_hires.h: sprites)")
|
|
sprites = sum(1 << sequence for sequence in sequences)
|
|
table.append(f'\t{{ "{tag}", {asset["bitmap"]}, {width}, {height}, 0x{asset["crc"]:08x}u, {coverage}, '
|
|
f'{point_threshold}, {int(title)}, 0x{sprites:x}u, asset{index}, {len(data)} }},')
|
|
lines.append("const struct hud_hires_embedded hud_hires_embedded[] =")
|
|
lines.append("{")
|
|
lines.extend(table)
|
|
if not table:
|
|
lines.append("\t{ 0 },")
|
|
lines.append("};")
|
|
lines.append(f"const unsigned int hud_hires_embedded_count = {len(table)};")
|
|
lines.append("")
|
|
# the fonts, and which draws each font tag (text_hires.h)
|
|
lines.append('#include "text_hires.h"')
|
|
lines.append("")
|
|
files = font_files()
|
|
for index, name in enumerate(files):
|
|
lines.append(f"static const unsigned int font{index}[] = {{")
|
|
lines.extend(words((ROOT / FONT_ASSETS / name).read_bytes()))
|
|
lines.append("};")
|
|
lines.append("")
|
|
fonts = json.loads((ROOT / FONT_LIST).read_text())["fonts"] if files else []
|
|
lines.append("const struct text_hires_embedded text_hires_embedded[] =")
|
|
lines.append("{")
|
|
for font in fonts:
|
|
index = files.index(font["file"])
|
|
tag = font["tag"].replace("\\", "\\\\")
|
|
size = (ROOT / FONT_ASSETS / font["file"]).stat().st_size
|
|
lines.append(f'\t{{ "{tag}", "{font["file"]}", font{index}, {size} }},')
|
|
if not fonts:
|
|
lines.append("\t{ 0 },")
|
|
lines.append("};")
|
|
lines.append(f"const unsigned int text_hires_embedded_count = {len(fonts)};")
|
|
lines.append("")
|
|
# the menus' files (menu_files.h)
|
|
lines.append('#include "menu_files.h"')
|
|
lines.append("")
|
|
menus = menu_files()
|
|
for index, name in enumerate(menus):
|
|
data = (ROOT / MENU_ASSETS / name).read_bytes()
|
|
if name.endswith(".png"):
|
|
png_size(data, name)
|
|
lines.append(f"static const unsigned int menu{index}[] = {{")
|
|
lines.extend(words(data))
|
|
lines.append("};")
|
|
lines.append("")
|
|
lines.append("const struct menu_file_embedded menu_files_embedded[] =")
|
|
lines.append("{")
|
|
for index, name in enumerate(menus):
|
|
size = (ROOT / MENU_ASSETS / name).stat().st_size
|
|
lines.append(f'\t{{ "{name}", menu{index}, {size} }},')
|
|
if not menus:
|
|
lines.append("\t{ 0 },")
|
|
lines.append("};")
|
|
lines.append(f"const unsigned int menu_files_embedded_count = {len(menus)};")
|
|
lines.append("")
|
|
# SMAA's shader, as text a GLSL compiler takes (ASCII, ending in a NUL),
|
|
# and its lookup textures (xgpu_post.c); each of size 0 that the
|
|
# checkout does not have. The OpenGL ES renderer has no SMAA.
|
|
lines.append("#ifndef HALO_GLES")
|
|
present = dict(smaa_files())
|
|
for name, symbol in SMAA_FILES:
|
|
data = (ROOT / SMAA_ASSETS / name).read_bytes() if name in present else b""
|
|
if data and name.endswith(".hlsl"):
|
|
data = bytes(byte if byte < 0x80 else 0x20 for byte in data) + b"\0"
|
|
lines.append("")
|
|
lines.append(f"const unsigned int {symbol}[] = {{")
|
|
lines.extend(words(data) if data else ["\t0,"])
|
|
lines.append("};")
|
|
lines.append(f"const unsigned long {symbol}_size = {len(data)};")
|
|
# the windows' icon, a PNG (of size 0 where the checkout has none)
|
|
data = (ROOT / WINDOW_ICON).read_bytes() if (ROOT / WINDOW_ICON).is_file() else b""
|
|
lines.append("")
|
|
lines.append("const unsigned int platform_window_icon[] = {")
|
|
lines.extend(words(data) if data else ["\t0,"])
|
|
lines.append("};")
|
|
lines.append(f"const unsigned long platform_window_icon_size = {len(data)};")
|
|
lines.append("")
|
|
lines.append("#endif")
|
|
output = Path(sys.argv[1])
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
text = "\n".join(lines) + "\n"
|
|
if not output.is_file() or output.read_text() != text:
|
|
output.write_text(text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|