singe/util/shootMenu.py
2026-09-22 18:32:07 -05:00

189 lines
5.9 KiB
Python

# Shoots the menu's pages for screenshots/menu: the game list and every service tool, through
# both of the menu's renderers -- the RmlUi document the GPU draws, and the plain overlay a machine
# without one falls back to (SDL_GPU_DRIVER=nothing). A driver script runs the bundled menu
# headless, waits for the intro, and presses the switches a person would: SERVICE for the tools,
# Button 1 into each one, Button 2 out, Down to the next. Each page is saved by name so a rerun
# replaces the picture, and the pictures of each renderer are tiled into one contact sheet.
#
# Usage: python3 util/shootMenu.py (both renderers)
# python3 util/shootMenu.py --renderer overlay
#
# The engine used is the gcc preset's, as util/renderMenuVideo.py uses; build it first. The
# menu shown is the one that binary carries, so a change to the menu's files needs a build before
# a shoot -- the script checks Menu.singe against assets and says so.
import argparse
import os
import shutil
import subprocess
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WIDTH = 1024 # The biggest the offscreen video driver gives.
HEIGHT = 768
RENDERERS = ["document", "overlay"]
SHEET_TILE = "420x315" # A quarter of the window, four to a row.
DRIVER = '''-- Runs the bundled menu and shoots its pages by name. Written by util/shootMenu.py.
dofile("Singe/Menu.singe")
local menuUpdate = onOverlayUpdate
local steps = {}
local at = 0
local settled = 0
local frames = 0
local function press(switch)
steps[#steps + 1] = { press = switch }
end
local function wait(count)
steps[#steps + 1] = { wait = count }
end
local function shoot(name)
steps[#steps + 1] = { shoot = name }
wait(3)
end
local function slug(name)
return name:lower():gsub("[^a-z0-9]+", "-")
end
-- The game list, then the tools: the list of them, and each in turn.
wait(30)
shoot("game-list")
press(SWITCH_DOWN)
wait(10)
shoot("game-list-second")
press(SWITCH_UP)
wait(10)
press(SWITCH_SERVICE)
wait(5)
shoot("00-service-tools")
for index, tool in ipairs(TOOLS) do
press(SWITCH_BUTTON1)
wait(45)
shoot(string.format("%02d-%s", index, slug(tool.name)))
press(SWITCH_BUTTON2)
wait(3)
press(SWITCH_DOWN)
wait(3)
end
press(SWITCH_SERVICE)
wait(5)
function onOverlayUpdate()
local result = menuUpdate()
frames = frames + 1
if MENU_RENDER.backdropReady() then
if at < #steps then
local step = steps[at + 1]
if step.wait then
settled = settled + 1
if settled >= step.wait then
settled = 0
at = at + 1
end
else
if step.press then
onInputPressed(step.press)
elseif step.shoot then
singeScreenshot(step.shoot)
end
at = at + 1
end
else
debugPrint("SHOOT done")
singeQuit()
end
end
if frames > 3000 then
debugPrint("SHOOT gave up waiting for the menu")
singeQuit()
end
return result
end
'''
def findBinary():
folder = os.path.join(REPO, ".builddir", "linux-gcc", "x86_64", "singe")
for name in sorted(os.listdir(folder)):
path = os.path.join(folder, name)
if name.startswith("Singe") and os.path.isfile(path) and os.access(path, os.X_OK):
return path
raise SystemExit("no Singe binary in %s; build first" % folder)
def sheet(renderer, folder):
names = sorted(f for f in os.listdir(folder) if f.endswith(".png"))
out = os.path.join(REPO, "screenshots", "menu", "sheet-%s.png" % renderer)
command = ["montage", "-label", "%t", "-tile", "4x", "-geometry", SHEET_TILE + "+8+8", "-background", "#202020", "-fill", "#e0e0e0"]
command += [os.path.join(folder, f) for f in names]
command.append(out)
subprocess.run(command, check=True)
return out
def shoot(binary, renderer):
work = os.path.join(REPO, ".builddir", "menuShots", renderer)
shots = os.path.join(work, "shots")
shutil.rmtree(shots, ignore_errors=True)
os.makedirs(shots)
# The menu finds its games in the folders beside it: the test scripts' games.dat is enough.
link = os.path.join(work, "testScripts")
if not os.path.islink(link):
os.symlink(os.path.join(REPO, "testScripts"), link)
with open(os.path.join(work, "shoot.singe"), "w") as out:
out.write(DRIVER)
env = dict(os.environ)
env["SDL_VIDEODRIVER"] = "offscreen"
env["SDL_AUDIO_DRIVER"] = "dummy"
env["SDL_AUDIODRIVER"] = "dummy"
if renderer == "overlay":
env["SDL_GPU_DRIVER"] = "nothing"
command = [binary, "-k", "-w", "-x", str(WIDTH), "-y", str(HEIGHT), "-d", shots + os.sep, "-v", "Singe/menuBackground.mkv", "shoot.singe"]
result = subprocess.run(command, cwd=work, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
text = result.stdout.decode("utf-8", "replace")
with open(os.path.join(work, "shoot.log"), "w") as out:
out.write(text)
if (result.returncode != 0) or ("SHOOT done" not in text):
sys.stdout.write(text)
raise SystemExit("the %s shoot did not finish; see %s" % (renderer, os.path.join(work, "shoot.log")))
mine = open(os.path.join(REPO, "assets", "Menu.singe"), "rb").read()
theirs = open(os.path.join(work, "Singe", "Menu.singe"), "rb").read()
if mine != theirs:
print("WARNING: the binary's Menu.singe is not the one in assets; rebuild and shoot again")
folder = os.path.join(REPO, "screenshots", "menu", renderer)
shutil.rmtree(folder, ignore_errors=True)
os.makedirs(folder)
names = sorted(f for f in os.listdir(shots) if f.endswith(".png"))
for name in names:
shutil.copyfile(os.path.join(shots, name), os.path.join(folder, name))
print("%s: %d pages in %s, sheet %s" % (renderer, len(names), folder, sheet(renderer, folder)))
def main():
parser = argparse.ArgumentParser(description="shoot the menu's pages through both renderers")
parser.add_argument("--renderer", choices=RENDERERS, help="just this one")
parser.add_argument("--binary", help="the engine to shoot with, in place of the built one")
args = parser.parse_args()
binary = args.binary or findBinary()
for renderer in ([args.renderer] if args.renderer else RENDERERS):
shoot(binary, renderer)
if __name__ == "__main__":
main()