# 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()