diff --git a/.gitignore b/.gitignore index 010770d68..eeb466b4a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,3 @@ docs/.asciidoctor/ /Menu.bat /data/ screenshots/ -screenshots-hw/ diff --git a/CHANGELOG b/CHANGELOG index 84190a622..77a941f93 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -354,10 +354,10 @@ API Changes a scrolling list on the left, up and down to select, left and right to page, Start or any button to play, and a mouse works too (click a row to select it, click it again or the Start button to play, wheel to - scroll the description). The audio delay calibration screen is a page - of the same document. The previous overlay-drawn menu is kept for one - release as Singe/MenuClassic.singe for a machine without a GPU device, - where guiNew fails; name it in Menu.sh or Menu.bat to use it. + scroll the description). The service tools are pages of the same + document. A machine without a GPU device, where guiNew fails, gets the + same menu drawn into the overlay instead; see "One menu, two renderers" + below. - Physics debug drawing. physicsSetDebug(DEBUG_*) draws the bodies in wireframe (coloured by motion state), joints and their limits, contact @@ -862,14 +862,36 @@ API Changes things that must not pass through each other -- which is what the collide calls deliberately do not do. +- One menu, two renderers. Singe/MenuClassic.singe is gone: it was the + whole menu written a second time, and the two copies duplicated their + game list, their selection, their menu.dat handling and their games.dat + walk almost line for line. Singe/Menu.singe is now everything the menu + is and draws nothing, and it picks one of two renderers at startup from + the new singeHasGpu(): Singe/MenuDocument.singe for a machine with a + GPU device, Singe/MenuOverlay.singe for one without. A machine with no + Vulkan, Direct3D 12 or Metal driver therefore gets the menu -- and all + ten service tools -- without anyone editing a launcher, which is what + editing Menu.sh used to be for. + + Both renderers take the same keys, so the overlay one now moves the + selection with up and down and pages with left and right, as the + document one does; it shows one game at a time rather than a list, and + clips a description too long for its column rather than scrolling it. + MENU_OVERLAY = true in menu.dat forces it on a machine that has a GPU. + + singeHasGpu() answers whether the GPU device came up. guiNew and the + scene calls end the script rather than returning false, so a game that + can do without one had no way to ask. + - The service tools. Pressing the SERVICE key on the bundled menu now opens a list of ten screens rather than the audio delay screen alone: Audio Delay, Input Test, System Information, Light Gun, Sound Test, Display, Disc Test, Saved Data, MIDI Ports and Frame Statistics. Button 2 backs out of a tool, the service key leaves. They live in - Singe/Tools.singe, which the engine extracts beside Menu.singe, and - each is a page of Singe/Menu.rml, so a cabinet builder can add one. - MenuClassic keeps the audio delay screen and nothing else. + Singe/Tools.singe, which the engine extracts beside Menu.singe. Nothing + there draws: a tool builds a list of rows and the menu draws them, so + the same ten run on both renderers and a cabinet builder adding one + writes no drawing code at all. Four calls were added for them, and are worth having on their own: diff --git a/CMakeLists.txt b/CMakeLists.txt index ee6aefcf4..42bfac46a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,7 +190,8 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "") -singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuClassic.singe ${GENERATED_DIR}/MenuClassic_singe.h "") +singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuDocument.singe ${GENERATED_DIR}/MenuDocument_singe.h "") +singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuOverlay.singe ${GENERATED_DIR}/MenuOverlay_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.rml ${GENERATED_DIR}/Menu_rml.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/menu.rcss ${GENERATED_DIR}/menu_rcss.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/gui.rcss ${GENERATED_DIR}/gui_rcss.h "") diff --git a/assets/Menu.rml b/assets/Menu.rml index 47ede87d3..80db6cef7 100644 --- a/assets/Menu.rml +++ b/assets/Menu.rml @@ -32,68 +32,11 @@ - - - - - - - - - - - ", class, tool.name) + rows[#rows + 1] = toolsItem(tool.name, i == TOOL_SELECTED) end - set("toolList", table.concat(rows)) - set("toolHelp", TOOLS[TOOL_SELECTED].help or "") + rows[#rows + 1] = toolsBlank() + rows[#rows + 1] = toolsText(TOOLS[TOOL_SELECTED].help or "") + rows[#rows + 1] = toolsKeys("Up / Down: choose Button 1: open Service: back to the games") + toolsShow(rows) end function toolsOpen(index) - TOOL_OPEN = TOOLS[index] + TOOL_OPEN = TOOLS[index] + TOOL_TITLE = TOOL_OPEN.name + TOOL_CLEAR = TOOL_OPEN.clear or false if TOOL_OPEN.begin then TOOL_OPEN.begin() end - toolsShowPage(TOOL_OPEN.id) + TOOL_DIRTY = true end @@ -99,9 +152,9 @@ function toolsClose() if TOOL_OPEN and TOOL_OPEN.finish then TOOL_OPEN.finish() end - TOOL_OPEN = nil + TOOL_OPEN = nil + TOOL_FLASH = false toolsShowList() - toolsShowPage("tools") end @@ -141,7 +194,7 @@ end -- Whether the tools have the screen, which is what the menu asks before doing anything of its own. function toolsActive() - return TOOL_OPEN ~= nil or not menuElement("tools"):IsClassSet("hidden") + return TOOL_SHOWING end @@ -158,12 +211,18 @@ local calValue, calBeat, calClickDone, calFlashDone local function audioShow() - set("calValue", tostring(calValue)) + toolsShow({ + toolsText("Adjust until the flash and the click happen together."), + toolsBlank(), + toolsPair("Delay", calValue .. " ms"), + toolsPair("Device queue", singeGetAudioLatency() .. " ms"), + toolsKeys("Left / Right: 10 ms Up / Down: 1 ms Start: reset to 0"), + toolsKeys("Button 1: save Button 2: back"), + }) end TOOLS[#TOOLS + 1] = { - id = "toolAudio", name = "Audio Delay", help = "Line the click up with the flash so sound and picture agree.", @@ -172,7 +231,6 @@ TOOLS[#TOOLS + 1] = { calBeat = singeGetTicks() - CAL_PERIOD calClickDone = true calFlashDone = true - set("calLatency", tostring(singeGetAudioLatency())) audioShow() end, @@ -218,14 +276,14 @@ TOOLS[#TOOLS + 1] = { end if not calFlashDone and (now - calBeat >= singeGetAudioLatency() + calValue) then calFlashDone = true - menuElement("toolAudio"):SetClass("flash", true) + toolsFlash(true) elseif calFlashDone then - menuElement("toolAudio"):SetClass("flash", false) + toolsFlash(false) end end, finish = function() - menuElement("toolAudio"):SetClass("flash", false) + toolsFlash(false) end, } @@ -251,7 +309,6 @@ end TOOLS[#TOOLS + 1] = { - id = "toolInput", name = "Input Test", help = "Controllers, keyboard, mice and light guns, live.", @@ -269,9 +326,9 @@ TOOLS[#TOOLS + 1] = { update = function() local rows = {} - rows[#rows + 1] = string.format("
Last switch: %s   count: %d
", inputLast, inputCount) - rows[#rows + 1] = string.format("
Gamepads: %d   Mice: %d   Stick drives mouse: %s
", - controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled())) + rows[#rows + 1] = toolsPair("Last switch", string.format("%s count: %d", inputLast, inputCount)) + rows[#rows + 1] = toolsPair("Devices", string.format("%d gamepads, %d mice, stick drives mouse: %s", + controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled()))) for slot = 0, SINGE_MAX_CONTROLLERS - 1 do if controllerIsValid(slot) then local axes = {} @@ -286,16 +343,17 @@ TOOLS[#TOOLS + 1] = { buttons[#buttons + 1] = tostring(b) end end - rows[#rows + 1] = string.format("
%d: %s
", slot, controllerGetName(slot)) - rows[#rows + 1] = string.format("
axes %s   buttons %s
", - table.concat(axes, ", "), (#buttons > 0) and table.concat(buttons, " ") or "-") + rows[#rows + 1] = toolsText(string.format("%d: %s", slot, controllerGetName(slot))) + rows[#rows + 1] = toolsSub(string.format("axes %s buttons %s", + table.concat(axes, ", "), (#buttons > 0) and table.concat(buttons, " ") or "-")) end end for m = 0, mouseHowMany() - 1 do local x, y = mouseGetPosition(m) - rows[#rows + 1] = string.format("
mouse %d: %s at %d, %d
", m, mouseGetName(m), x, y) + rows[#rows + 1] = toolsSub(string.format("mouse %d: %s at %d, %d", m, mouseGetName(m), x, y)) end - set("inputRows", table.concat(rows)) + rows[#rows + 1] = toolsKeys("Press anything. Button 2: back") + toolsShow(rows) end, } @@ -305,29 +363,26 @@ TOOLS[#TOOLS + 1] = { -- Everything the trace header knows, on one screen. A photograph of this answers most of a bug -- report without anyone having to find trace.txt, which is the point of it. TOOLS[#TOOLS + 1] = { - id = "toolSystem", name = "System Information", help = "What this machine is and what the engine chose to run on.", begin = function() - local i = singeGetSystemInfo() - local rows = {} - local function line(label, value) - rows[#rows + 1] = string.format("
%s%s
", label, tostring(value)) - end - line("Singe", i.version) - line("System", i.os) - line("Processor", i.cpu) - line("Renderer", i.renderer) - line("3D device", i.gpu) - line("Video", i.decoder) - line("Audio", i.audio) - line("SoundFont", i.soundFont) - line("MIDI", i.midi) - line("Window", string.format("%d x %d, canvas %d x %d, overlay %d x %d", - i.windowWidth, i.windowHeight, i.canvasWidth, i.canvasHeight, overlayGetWidth(), overlayGetHeight())) - line("Data", i.dataPath) - set("systemRows", table.concat(rows)) + local i = singeGetSystemInfo() + toolsShow({ + toolsPair("Singe", i.version), + toolsPair("System", i.os), + toolsPair("Processor", i.cpu), + toolsPair("Renderer", i.renderer), + toolsPair("3D device", i.gpu), + toolsPair("Video", i.decoder), + toolsPair("Audio", i.audio), + toolsPair("SoundFont", i.soundFont), + toolsPair("MIDI", i.midi), + toolsPair("Window", string.format("%d x %d, canvas %d x %d, overlay %d x %d", + i.windowWidth, i.windowHeight, i.canvasWidth, i.canvasHeight, overlayGetWidth(), overlayGetHeight())), + toolsPair("Data", i.dataPath), + toolsKeys("Button 2: back"), + }) end, } @@ -337,6 +392,7 @@ TOOLS[#TOOLS + 1] = { -- Where the gun is pointing, against the picture it is pointing at. A cabinet needs this every -- time the gun or the monitor moves, and nothing else in the engine shows the mapping from the -- gun's own coordinates to the canvas. +-- -- The targets: one in the middle and one in each corner, set in far enough that a gun that is -- slightly out still puts its shot on the screen beside them. local GUN_SHOTS_KEPT = 12 @@ -348,14 +404,23 @@ local gunShots = {} local gunLast = "none yet" +local function gunShow() + toolsShow({ + toolsPair("Last shot", string.format("%s shots shown: %d", gunLast, #gunShots)), + toolsKeys("Button 1: fire Start: clear Button 2: back"), + }) +end + + TOOLS[#TOOLS + 1] = { - id = "toolGun", - name = "Light Gun", - help = "Aim and fire: see where the shot actually lands.", + name = "Light Gun", + help = "Aim and fire: see where the shot actually lands.", + clear = true, begin = function() gunShots = {} gunLast = "none yet" + gunShow() end, input = function(what) @@ -366,9 +431,11 @@ TOOLS[#TOOLS + 1] = { table.remove(gunShots, 1) end gunLast = string.format("%d, %d", x, y) + gunShow() return true elseif what == SWITCH_START1 or what == SWITCH_START2 then gunShots = {} + gunShow() return true end return false @@ -394,8 +461,6 @@ TOOLS[#TOOLS + 1] = { overlayLine(shot.x - GUN_CROSS, shot.y, shot.x + GUN_CROSS, shot.y) overlayLine(shot.x, shot.y - GUN_CROSS, shot.x, shot.y + GUN_CROSS) end - set("gunLast", gunLast) - set("gunCount", tostring(#gunShots)) end, finish = function() @@ -411,22 +476,34 @@ TOOLS[#TOOLS + 1] = { local soundStep = 0 local soundNames = { "left speaker", "right speaker", "both speakers" } local soundPans = { -1.0, 1.0, 0.0 } +local soundWhich = "" +local soundList = "" + + +local function soundShow() + toolsShow({ + toolsPair("Playing", soundWhich), + toolsPair("Disc tracks", soundList), + toolsKeys("Button 1: left, right, both Left / Right: disc track Button 2: back"), + }) +end TOOLS[#TOOLS + 1] = { - id = "toolSound", name = "Sound Test", help = "Left, right, both, and the disc's own audio tracks.", begin = function() - soundStep = 0 - set("soundWhich", "press Button 1") local tracks = {} + + soundStep = 0 + soundWhich = "press Button 1" for t = 0, discGetAudioTracks() - 1 do local code = discGetLanguage(t) tracks[#tracks + 1] = string.format("%d:%s", t, (code ~= "" and code ~= "unk") and code or "unnamed") end - set("soundTracks", (#tracks > 0) and table.concat(tracks, "   ") or "the disc has none") + soundList = (#tracks > 0) and table.concat(tracks, " ") or "the disc has none" + soundShow() end, input = function(what) @@ -434,15 +511,16 @@ TOOLS[#TOOLS + 1] = { soundStep = (soundStep % #soundNames) + 1 soundSetPan(SND_CLICK, soundPans[soundStep]) soundPlay(SND_CLICK) - set("soundWhich", soundNames[soundStep]) + soundWhich = soundNames[soundStep] + soundShow() return true elseif what == SWITCH_LEFT or what == SWITCH_RIGHT then - -- Walk the disc's audio tracks, which is how a multi language release is checked. local count = discGetAudioTracks() if count > 1 then local next = (discGetAudioTrack() + ((what == SWITCH_RIGHT) and 1 or (count - 1))) % count discSetAudioTrack(next) - set("soundWhich", string.format("disc track %d (%s)", next, discGetLanguage(next))) + soundWhich = string.format("disc track %d (%s)", next, discGetLanguage(next)) + soundShow() end return true end @@ -470,23 +548,29 @@ local DISP_ROWS = 12 local DISP_STEPS = 8 local DISP_STEP_WIDE = 32 local DISP_WEDGE = 16 +local dispSaved = "" local function displayShow() local x, y = vldpGetShift() - set("dispValues", string.format("scale %d%%   shift %d, %d   rotation %d", - vldpGetScale(), x, y, vldpGetRotate())) + toolsShow({ + toolsPair("Picture", string.format("scale %d%% shift %d, %d rotation %d", + vldpGetScale(), x, y, vldpGetRotate())), + toolsPair("Kept", dispSaved), + toolsKeys("Arrows: move Button 3 / 4: size Coin 1: rotate Start: reset"), + toolsKeys("Button 1: keep for this machine Button 2: back"), + }) end TOOLS[#TOOLS + 1] = { - id = "toolDisplay", - name = "Display", - help = "Size, position and rotation of the picture, and a test pattern.", + name = "Display", + help = "Size, position and rotation of the picture, and a test pattern.", + clear = true, begin = function() + dispSaved = "not yet" displayShow() - set("dispSaved", "") end, input = function(what) @@ -511,13 +595,14 @@ TOOLS[#TOOLS + 1] = { vldpSetRotate(0) elseif what == SWITCH_BUTTON1 then singeSaveGeometry() - set("dispSaved", "kept for this machine") + dispSaved = "kept for this machine" + displayShow() return true else return false end + dispSaved = "not yet" displayShow() - set("dispSaved", "") return true end, @@ -563,6 +648,7 @@ TOOLS[#TOOLS + 1] = { -- How long the disc takes to find a frame. A rip with its keyframes far apart seeks slowly, and -- the only symptom in a game is that everything feels heavy; this measures it directly. +-- -- Twelve seeks spread over the disc, and a seek that has not landed in three seconds has failed -- rather than been slow. local DISC_SEEKS = 12 @@ -572,8 +658,21 @@ local discPending = nil local discWorst = 0 +local function discShow(note) + local rows = {} + + if note ~= nil then + rows[#rows + 1] = toolsText(note) + end + for _, line in ipairs(discResults) do + rows[#rows + 1] = line + end + rows[#rows + 1] = toolsKeys("Button 1: run Button 2: back") + toolsShow(rows) +end + + TOOLS[#TOOLS + 1] = { - id = "toolDisc", name = "Disc Test", help = "Seek timing: how quickly the disc finds a frame.", @@ -581,7 +680,7 @@ TOOLS[#TOOLS + 1] = { discResults = {} discPending = nil discWorst = 0 - set("discRows", "
Button 1 runs the test.
") + discShow("Button 1 runs the test.") end, input = function(what) @@ -589,6 +688,7 @@ TOOLS[#TOOLS + 1] = { discResults = {} discWorst = 0 discPending = { step = 0, started = 0 } + discShow("Running.") return true end return false @@ -610,14 +710,16 @@ TOOLS[#TOOLS + 1] = { end if discGetFrame() == discPending.target or singeGetTicks() - discPending.started > DISC_SEEK_LIMIT then local took = singeGetTicks() - discPending.started - discResults[#discResults + 1] = string.format("
frame %d took %d ms
", discPending.target, took) + discResults[#discResults + 1] = toolsSub(string.format("frame %d took %d ms", discPending.target, took)) discWorst = math.max(discWorst, took) discPending.started = 0 if discPending.step >= DISC_SEEKS then - discResults[#discResults + 1] = string.format("
worst %d ms over %d seeks
", discWorst, discPending.step) + discResults[#discResults + 1] = toolsText(string.format("worst %d ms over %d seeks", discWorst, discPending.step)) discPending = nil + discShow(nil) + else + discShow("Running.") end - set("discRows", table.concat(discResults)) end end, @@ -638,26 +740,25 @@ local saveSelected = 1 local function savedShow() local rows = {} + if #saveDirs == 0 then - rows[#rows + 1] = "
No game has saved anything yet.
" + rows[#rows + 1] = toolsText("No game has saved anything yet.") end for i, entry in ipairs(saveDirs) do - local class = (i == saveSelected) and " class='selected'" or "" - rows[#rows + 1] = string.format("%s   %d bytes", class, entry.name, entry.size) + rows[#rows + 1] = toolsItem(string.format("%s %d bytes", entry.name, entry.size), i == saveSelected) end - set("savedRows", table.concat(rows)) + rows[#rows + 1] = toolsKeys("Up / Down: choose Button 3: delete this game's save Button 2: back") + toolsShow(rows) end TOOLS[#TOOLS + 1] = { - id = "toolSaved", name = "Saved Data", help = "What each game has kept, and clearing it.", begin = function() saveDirs = {} saveSelected = 1 - -- Every game's data directory is one below the root the engine was given. -- lfs.dir hands back its iterator and the directory it walks; the iterator is useless -- without it, so both are kept. pcall because the root need not exist yet. local root = singeGetSystemInfo().dataRoot @@ -707,6 +808,7 @@ TOOLS[#TOOLS + 1] = { -- The ports this machine has, and whether anything is coming in or going out. Nothing is opened -- until this tool asks, so a cabinet that never touches MIDI never pays for it. +-- -- Middle C on channel 1 at a plain mezzo-forte: loud enough to hear on any instrument, and on -- the channel a synthesiser is most likely to have something assigned to. local MIDI_TEST_CHANNEL = 1 @@ -720,17 +822,19 @@ local midiDirty = false local function midiShow() local rows = {} - rows[#rows + 1] = string.format("
%d in, %d out
", midiInputCount(), midiOutputCount()) + + rows[#rows + 1] = toolsPair("Ports", string.format("%d in, %d out", midiInputCount(), midiOutputCount())) for i = 0, midiOutputCount() - 1 do - rows[#rows + 1] = string.format("
out %d%s %s
", i, (i == midiOut) and " *" or "", midiOutputName(i)) + rows[#rows + 1] = toolsSub(string.format("out %d%s %s", i, (i == midiOut) and " *" or "", midiOutputName(i))) end for i = 0, midiInputCount() - 1 do - rows[#rows + 1] = string.format("
in  %d%s %s
", i, (i == midiIn) and " *" or "", midiInputName(i)) + rows[#rows + 1] = toolsSub(string.format("in %d%s %s", i, (i == midiIn) and " *" or "", midiInputName(i))) end for _, m in ipairs(midiSeen) do - rows[#rows + 1] = string.format("
received %s
", m) + rows[#rows + 1] = toolsSub("received " .. m) end - set("midiRows", table.concat(rows)) + rows[#rows + 1] = toolsKeys("Button 1: send a note Button 3: look again Button 2: back") + toolsShow(rows) end @@ -744,7 +848,6 @@ end TOOLS[#TOOLS + 1] = { - id = "toolMidi", name = "MIDI Ports", help = "List the ports, send a note, watch what arrives.", @@ -790,19 +893,25 @@ TOOLS[#TOOLS + 1] = { -- ===== 10. Frame statistics ===== +local function statsShow() + toolsShow({ + toolsPair("Overlay", statsIsEnabled() and "on" or "off"), + toolsText("It stays on while you play, in the top left corner."), + toolsKeys("Button 1: turn it on or off Button 2: back"), + }) +end + + TOOLS[#TOOLS + 1] = { - id = "toolStats", name = "Frame Statistics", help = "The developer's overlay: frame time, what is alive, what the disc is doing.", - begin = function() - set("statsState", statsIsEnabled() and "on" or "off") - end, + begin = statsShow, input = function(what) if what == SWITCH_BUTTON1 then statsEnable(not statsIsEnabled()) - set("statsState", statsIsEnabled() and "on" or "off") + statsShow() return true end return false diff --git a/assets/menu.rcss b/assets/menu.rcss index a46584552..19d785805 100644 --- a/assets/menu.rcss +++ b/assets/menu.rcss @@ -215,11 +215,21 @@ body { word-break: break-word; } -/* The chosen line of a list the tool draws, in the game list's gold. */ -.tool .selected { +/* A line a tool offers to choose between, and the chosen one in the game list's gold -- the same + way the game list itself shows a selection, since RCSS has no generated content to mark it with. */ +.tool .item { + margin-bottom: 2dp; +} + +.tool .item.selected { color: #ffd35a; } +/* A gap a tool asked for. */ +.tool .blank { + height: 12dp; +} + /* Two tools draw their own picture into the overlay, which is underneath the document: the light gun's targets and the display's test pattern. Those pages let it through and gather their text into a strip along the bottom, between the two lower targets, so nothing worth aiming at is @@ -238,6 +248,10 @@ body { } .tool.clear h1, +.tool.clear .row, +.tool.clear .pair, +.tool.clear .sub, +.tool.clear .item, .tool.clear p { background-color: #000000c0; padding: 1dp 8dp; diff --git a/docs/Manual.adoc b/docs/Manual.adoc index aae03adbf..4ccf7303e 100644 --- a/docs/Manual.adoc +++ b/docs/Manual.adoc @@ -94,14 +94,38 @@ click on a row selects that game and a second click on the same row, or a click on the Start button in the footer, launches it; the mouse wheel scrolls a description too long for its panel. The menu keeps its selection in `menu.dat` in its data directory, so it comes back to the same game when -one quits or when Singe is next started. +one quits or when Singe is next started. That file is a Lua fragment the menu +reads at startup and rewrites at shutdown: `GAME_SELECTED` is the game it was +showing, `SHOW_INTRO` is false when it is returning from a game so the intro +is not sat through twice, and `MENU_OVERLAY` chooses a renderer (see +<>). -A GUI needs the GPU device the 3D scene uses, so on a machine without one -`guiNew` fails and the menu cannot start. `Singe/MenuClassic.singe` is the -previous menu, drawn with sprites and `fontPrint` straight into the overlay, -kept for one release for exactly that machine: edit `Menu.sh` (or `Menu.bat`) -to name it in place of `Singe/Menu.singe`. It has the audio delay screen and -nothing else; the rest of the service tools need the document. +[#menurenderers] +==== The two renderers + +A GUI document is drawn through the GPU device the 3D scene uses, so on a +machine without one there is no document to build. The menu handles that +itself: `Singe/Menu.singe` asks <> at startup and +loads one of two renderers. + +`Singe/MenuDocument.singe`:: +The RmlUi document described above, for a machine with a GPU device. + +`Singe/MenuOverlay.singe`:: +Sprites and `fontPrint` straight into the overlay, the look Singe 2 had. It +needs nothing beyond the overlay every game already draws into. + +Both draw the same menu, take the same keys and run the same service tools; +`Singe/Menu.singe` holds everything that is not drawing -- the game list, the +selection, launching a game -- so there is one menu rather than two. The +overlay renderer shows one game at a time instead of a list, since it has no +panel to scroll, and it clips a description too long for its column rather +than scrolling it. + +To see the overlay renderer on a machine that does have a GPU, put +`MENU_OVERLAY = true` in `menu.dat` in the menu's data directory; the menu +writes the line back out, so it survives. That is how the overlay path is +tested, and how a cabinet builder who prefers the old look gets it. [#servicetools] ==== The Service Tools @@ -109,9 +133,11 @@ nothing else; the rest of the service tools need the document. The key mapped to `INPUT_SERVICE` opens a list of tools over the game list. Up and down choose one and button 1 opens it; button 2 backs out of a tool to the list, and the service key again leaves the tools altogether. Everything -lives in `Singe/Tools.singe`, which the engine extracts beside `Menu.singe`, -and each tool is a page of `Singe/Menu.rml`. Nothing here needs a game to be -running, and nothing changes a game's own files. +lives in `Singe/Tools.singe`, which the engine extracts beside `Menu.singe`. +Nothing there draws: a tool builds a list of rows and hands it to the menu, +which is why the same ten tools run on both renderers (see +<>). Nothing here needs a game to be running, +and nothing changes a game's own files. Audio Delay:: The downstream audio delay, measured against a click and a flash. See @@ -947,10 +973,11 @@ longer there. Singe/ Support files extracted by the engine Framework.singe Loaded by every game (dofile it) Menu.singe The bundled game menu - Menu.rml The menu's RmlUi document - menu.rcss The menu's style sheet, on top of gui.rcss Tools.singe The menu's service tools (see The Service Tools) - MenuClassic.singe The previous, overlay-drawn menu (see The Bundled Menu) + MenuDocument.singe The menu drawn as a GUI document + MenuOverlay.singe The menu drawn into the overlay, for a machine with no GPU + Menu.rml The document renderer's RmlUi document + menu.rcss Its style sheet, on top of gui.rcss gui.rcss The shipped GUI theme controls.cfg.example Template for input mappings click.wav Used by the menu's audio delay and sound tests @@ -1419,9 +1446,10 @@ texture goes wherever a texture can: composited over the overlay with the same options page works flat on the screen and on the monitor of a modeled arcade cabinet. A GUI needs the GPU device the 3D scene uses, so it is unavailable on a machine without one (see <>); `guiNew` raises an error there. The bundled menu is such -a document, which is why `Singe/MenuClassic.singe` exists (see -<>). Every call named here has an entry in the +and Requirements>>); `guiNew` raises an error rather than answering false, so +ask <> first if your game can do without. The bundled +menu does exactly that, and draws itself into the overlay instead when the +answer is no (see <>). Every call named here has an entry in the <> section of the reference. The language reference for RML and RCSS is RmlUi's own documentation at @@ -14227,6 +14255,40 @@ else end ---- +[#singehasgpu] +==== singeHasGpu + +[source,text] +---- +present = singeHasGpu() +---- + +Whether the GPU device came up. Everything drawn through it is unavailable +without one: the 3D scene, the particles, and every GUI document. Ask this +before asking for any of them, because <> and the scene calls +end the script rather than answering `false` -- there is no way to try and +recover. + +A machine can lack one because it has no Vulkan, Direct3D 12 or Metal driver +at all, or because `--nogpu` was given. The bundled menu calls this at startup +and picks a renderer from the answer (see <>). + +*Returns:* boolean, true when a GPU device is present. + +*Since:* 3.00. +*See also:* <>, <>, <> + +.Example +[source,lua] +---- +-- A game with a 3D title screen and a flat one to fall back on. +if singeHasGpu() then + buildSceneTitle() +else + buildSpriteTitle() +end +---- + [#singequit] ==== singeQuit diff --git a/src/embedded.h b/src/embedded.h index e988ee6cb..e79d02c1a 100644 --- a/src/embedded.h +++ b/src/embedded.h @@ -34,7 +34,8 @@ #include "generated/settings_cfg.h" #include "generated/Menu_singe.h" #include "generated/Tools_singe.h" -#include "generated/MenuClassic_singe.h" +#include "generated/MenuDocument_singe.h" +#include "generated/MenuOverlay_singe.h" #include "generated/Menu_rml.h" #include "generated/menu_rcss.h" #include "generated/gui_rcss.h" diff --git a/src/main.c b/src/main.c index 7b5a57d3e..65e8ad2bd 100644 --- a/src/main.c +++ b/src/main.c @@ -1791,7 +1791,8 @@ static void _unpackData(const char *exePath, bool absolute) { { "settings.cfg.example", settings_cfg, settings_cfg_len }, { "Menu.singe", Menu_singe, Menu_singe_len }, { "Tools.singe", Tools_singe, Tools_singe_len }, - { "MenuClassic.singe", MenuClassic_singe, MenuClassic_singe_len }, + { "MenuDocument.singe", MenuDocument_singe, MenuDocument_singe_len }, + { "MenuOverlay.singe", MenuOverlay_singe, MenuOverlay_singe_len }, { "Menu.rml", Menu_rml, Menu_rml_len }, { "menu.rcss", menu_rcss, menu_rcss_len }, { "FreeSansBold.ttf", FreeSansBold_ttf, FreeSansBold_ttf_len }, diff --git a/src/singe.c b/src/singe.c index 9faafb9cb..016dc7015 100644 --- a/src/singe.c +++ b/src/singe.c @@ -1275,6 +1275,7 @@ static int32_t apiSingeGetPauseFlag(lua_State *L); static int32_t apiSingeGetScriptPath(lua_State *L); static int32_t apiSingeGetTicks(lua_State *L); static int32_t apiSingeGetWidth(lua_State *L); +static int32_t apiSingeHasGpu(lua_State *L); static int32_t apiSingeQuit(lua_State *L); static int32_t apiSingeReload(lua_State *L); static int32_t apiSingeScreenshot(lua_State *L); @@ -5478,6 +5479,7 @@ static void _registerApi(lua_State *L) { lua_register(L, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG lua_register(L, "singeGetTicks", apiSingeGetTicks); // 3.00 lua_register(L, "singeGetWidth", apiSingeGetWidth); // 1.xx + lua_register(L, "singeHasGpu", apiSingeHasGpu); // 3.00 lua_register(L, "singeQuit", apiSingeQuit); // 1.xx RDG lua_register(L, "singeReload", apiSingeReload); // 3.00 lua_register(L, "singeSaveGeometry", apiSingeSaveGeometry); // 3.00 @@ -12724,6 +12726,20 @@ static int32_t apiSingeGetWidth(lua_State *L) { // singeQuit() +// Whether the GPU device came up. Everything drawn through it -- the 3D scene, the particles and +// every GUI document -- is unavailable without one, and guiNew ends the script rather than answering +// false, so a script that can do without has to ask before it asks for a GUI. +static int32_t apiSingeHasGpu(lua_State *L) { + bool has = (_global.device != NULL); + + _argCheck(L, "singeHasGpu", 0, 0); + _luaTrace(L, "singeHasGpu", "%s", has ? "true" : "false"); + lua_pushboolean(L, has); + + return 1; +} + + static int32_t apiSingeQuit(lua_State *L) { _luaTrace(L, "singeQuit", "Quit requested."); _global.running = false;