8 Commits
Author SHA1 Message Date
TheRealRetro 89786fd668 Background from the full-size original (5504x3072), sharper at 4K
The same picture as before, cleaned the same way, scaled down to 3840x2160
instead of up from a 2000-pixel copy.
2026-10-17 00:47:22 -04:00
TheRealRetro f97346904d New home screen icon and background
The icon and background are pictures now (app/art/icon.png and
background.png), a retro controller in the menus' violet; the background's
top left, where the console draws the app's name and buttons, is plain.
tools/make_art.py --images fits them to icon0.png (512x512) and
background-source.png (3840x2160) and writes the BC7 pic0/pic1.dds.
2026-10-17 00:45:04 -04:00
TheRealRetro 0ec5920b37 0.7.4 release notes: the new menus, quick load and save 2026-10-17 00:26:18 -04:00
TheRealRetro 0ca7ea5721 Menus in the new look; touchpad-only pause menu, L3/R3 quick load/save
The pause menu, Settings on the game list and the Controller Layout and
Cheats pages are one panel over the paused game or the game list: a header
with the title, rows with an icon, a value box, a switch or a slider, and a
footer of buttons (the user's mockup). frontend/fe_draw.cpp draws the
anti-aliased rounded panels, glows, gradients, switches and a few icons on
the CPU; fe_text gains pixel sizes and a bold. The static parts are drawn
once per menu and copied each frame.

Pause menu: the touchpad resumes, Square resets the game after a question,
Triangle goes back to the game list; Save State, Load State and Quit take
Cross. The game list's own settings stay in Settings there.

In a game the touchpad alone opens the pause menu; R3 saves in the current
state slot and L3 loads it, so they are out of the controller layout.
2026-10-17 00:26:06 -04:00
TheRealRetro 240baec05e Cheats: the core's GameShark code list in the pause menu
frontend/fe_cheats.cpp reads mupen64plus-core's data/mupencheat.txt (558
game versions, 9238 cheats), built into eboot.bin zlib-compressed (235 KB),
and finds a game by its ROM header's CRCs and country. The pause menu's
Cheats page switches each cheat off or on, or picks one of its choices,
with the database's note beside it. The core applies them every VI
(CoreAddCheat, CoreCheatEnabled); the ones left on are on again next time
(cheats/<ROM file>.txt). Codes that need the GameShark's button are left
out: the DualSense has none.
2026-10-16 22:46:08 -04:00
TheRealRetro 7b2a21208e Releases tagged 0.7.4 instead of 00.007.004
The tag on GitHub is the version as people write it. The contentVersion
stays NN.NNN.NNN (the console needs it), and every app since 0.6.0 reads both
tag styles as numbers (ParseVersion), so updates still compare correctly.
release.ps1 refuses a version already released under either tag style.
2026-10-16 22:03:16 -04:00
TheRealRetro 9e3b6c1596 0.7.4: controller layouts per game, volume setting 2026-10-16 21:58:06 -04:00
TheRealRetro bca88bf154 Controller layouts per game, volume setting
Controller layout (Settings): each DualSense control -- Cross, Circle,
Square, Triangle, L1, R1, L2 and R2 separately, L3, R3, OPTIONS, the D-pad
and the right stick's four directions -- presses the N64 button the layout
gives it, or nothing; the left stick stays the analog stick. From the game
list it is every game's layout (layout= in mupen64plus-ps5.ini); from the
pause menu, the running game's own (layouts/<ROM file>.txt, gone again when
it matches every game's). The default is the old fixed mapping.

Volume (Settings): the game's sound at 0-100%, live from the pause menu.

Asked for on GBAtemp: L2 and R2 both sent Z, so a game's own control setup
couldn't tell them apart.
2026-10-16 21:57:58 -04:00
27 changed files with 1943 additions and 208 deletions

No files matched your search

+2 -1
View File
@@ -72,7 +72,8 @@ The GPU renderer needs RADV built first (mihawk-99/PS5_Vulkan, in WSL `/root/gpu
Releasing:
1. Raise `VERSION` in `ps5/Makefile`, and set the matching `contentVersion` in `ps5/app/sce_sys/param.json`
(0.7.0 → `00.007.000`; the build stops if they differ). That number is also the release tag.
(0.7.0 → `00.007.000`; the build stops if they differ). The release tag is the version as people write it
(`0.7.4`; up to 0.7.3 it was the `contentVersion`, `00.007.003`). The app reads both as numbers.
2. Write `release-notes/<contentVersion>.md`: a few short lines, shown on the console in the update question.
3. Commit and push, then run `release.bat`. It builds, creates the release with `Mupen64PlusPS5.zip`, and checks
GitHub's digest against the local zip.
+10 -2
View File
@@ -33,7 +33,7 @@
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
PS5_HOST ?= ps5
PS5_PORT ?= 9021
VERSION ?= 0.7.3
VERSION ?= 0.7.4
# param.json's contentVersion, NN.NNN.NNN (0.6.1 -> 00.006.001): the release tag, as in the Homebrew Browser; it
# must also be written in app/sce_sys/param.json (checked below)
CONTENT_VERSION := $(shell echo $(VERSION) | awk -F. '{printf "%02d.%03d.%03d", $$1, $$2, $$3}')
@@ -123,7 +123,7 @@ EMU_SRC := coreorbis/main-boot.cpp \
coreorbis/orbis-shims/ProsperoCrash.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp \
frontend/fe_emu.cpp frontend/fe_menu.cpp frontend/fe_settings.cpp frontend/fe_text.cpp \
frontend/fe_games.cpp frontend/fe_http.cpp frontend/fe_covers.cpp frontend/fe_shelf.cpp \
frontend/fe_prefetch.cpp frontend/fe_update.cpp
frontend/fe_prefetch.cpp frontend/fe_update.cpp frontend/fe_cheats.cpp frontend/fe_draw.cpp
# the payloads: the helper, and the installer (= helper + install)
HELPER_SRC := coreorbis/orbis-shims/orbis_paths.cpp coreorbis/orbis-shims/ProsperoNotify.cpp \
coreorbis/orbis-shims/ProsperoHelper.cpp coreorbis/orbis-shims/ProsperoUpdateJob.cpp
@@ -155,6 +155,13 @@ $(GAMEDB): tools/make_n64db.py $(M64_INI) $(BOXARTS)
%/port/frontend/fe_games.o: EXTRA_FLAGS = -DGAMEDB_TSV='"$(abspath $(GAMEDB))"'
# the ROM catalog itself goes into fe_emu.o (written to /data/mupen64plus/data for the core)
%/port/frontend/fe_emu.o: EXTRA_FLAGS = -DM64P_INI_PATH='"$(abspath $(M64_INI))"'
# the core's cheat database (1.2 MB of text), compressed into fe_cheats.o: 4 bytes of size, then zlib
CHEATS_Z := build/gen/mupencheat.z
$(CHEATS_Z): $(M64)/data/mupencheat.txt
@mkdir -p $(dir $@)
python3 -c "import struct, sys, zlib; d = open(sys.argv[1], 'rb').read(); \
open(sys.argv[2], 'wb').write(struct.pack('<I', len(d)) + zlib.compress(d, 9))" $< $@
%/port/frontend/fe_cheats.o: EXTRA_FLAGS = -DCHEATS_Z='"$(abspath $(CHEATS_Z))"'
# stb_image in fe_covers.o has warnings of its own
%/port/frontend/fe_covers.o: EXTRA_FLAGS = -Wno-error -Wno-sign-compare -Wno-unused-but-set-variable -Wno-implicit-fallthrough
# the UI fonts (PS5SX2's: PCSX2's Roboto Regular and PromptFont) built into fe_text.o
@@ -597,6 +604,7 @@ $(HOST_OUT)/mupen64plus-ps5-host: $(HOST_CORE_OBJ) $(HOST_EMU_OBJ) $(HOST_ZLIB_O
# the files built into fe_games.o / fe_emu.o
$(PS5_OUT)/port/frontend/fe_games.o $(HOST_OUT)/port/frontend/fe_games.o: $(GAMEDB)
$(PS5_OUT)/port/frontend/fe_emu.o $(HOST_OUT)/port/frontend/fe_emu.o: $(M64_INI)
$(PS5_OUT)/port/frontend/fe_cheats.o $(HOST_OUT)/port/frontend/fe_cheats.o: $(CHEATS_Z)
clean:
rm -rf build
+24 -9
View File
@@ -143,7 +143,8 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
## Controls
**In a game** (buttons by position, as on the N64 pad)
**In a game** (the default layout: buttons by position, as on the N64 pad; Settings → Controller layout changes
it, for every game or, from the pause menu, for one game)
| PS5 | N64 |
|---|---|
@@ -155,14 +156,24 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
| right stick | C buttons |
| Triangle / Circle | C-up / C-down |
| OPTIONS | Start |
| **touchpad click** or **L3 + R3** | pause menu |
| **touchpad click** | pause menu |
| **R3** (press the right stick) | save the state in the current slot |
| **L3** (press the left stick) | load the state of the current slot |
**The pause menu** has:
**The pause menu** (the touchpad) is a panel over the paused game, drawn on the CPU with the anti-aliased shapes
of `frontend/fe_draw.cpp`; Settings on the game list and the Controller Layout and Cheats pages share its look
(`fe_menu.cpp`, namespace `pm`). Its footer's buttons work anywhere in it: touchpad resume, Square reset the
game (after a question), Triangle back to the game list, Cross confirm, Circle back (resume). Its rows:
- resume, save state, load state, and the state slot (0–9);
- save state and load state (Cross), and the state slot (0–9);
- **cheats**: the GameShark codes of the core's `data/mupencheat.txt` (about 560 game versions, found by the ROM
header's CRCs and country; built into `eboot.bin` compressed, `frontend/fe_cheats.cpp`). Each one is off or on,
or off or one of its choices, with the database's note beside it; the ones left on are on again the next time
(`cheats/<ROM file>.txt`). Codes that need the GameShark's button are left out;
- speed (normal or fast forward);
- every setting below;
- reset, back to the game list, and quit.
- every setting below but the game list's (Continue where you left off, Game list view, Download covers, Check
for updates: Settings on the game list has them);
- quit (Cross).
**Settings** (marked `*` when they apply from the next game start):
@@ -183,9 +194,11 @@ Title ID `PPSA99064` and the folders below don't overlap with Snes9x PS5 (PPSA99
| Stick dead zone | 0–30% |
| Invert left stick | turns the analog stick's X (left/right), Y (up/down) or both round; Off by default |
| Invert right stick | the same for the right stick's C buttons (Triangle / Circle stay C-up / C-down); Off by default |
| Controller layout | what each DualSense control presses on the N64 pad: Cross, Circle, Square, Triangle, L1, R1, L2, R2 (separately), OPTIONS, the D-pad and the right stick's four directions, each set to an N64 button or nothing (the left stick stays the analog stick). From the game list it is every game's layout (in `mupen64plus-ps5.ini`); from the pause menu, the running game's own (`layouts/<ROM file>.txt`, gone again when it matches every game's) |
| Continue where you left off | Ask (default): leaving a game from the pause menu saves where you were (`states/<ROM file>.resume`), and starting it again asks "Continue where you left off?" (✕ continue, □ start over, ○ back). Off: neither |
| Game list view | Shelf (3D, default), Grid or List; the touchpad on the game list switches too |
| Show FPS | the speed (`VI/s`, 100% = full speed) and, below it, where the emulation thread's time goes: `cpu` (VR4300 interpreter), `rsp` (rsp-hle + cxd4), `rdp` (the renderer: angrylion's rasteriser incl. waiting for its threads, or handing commands to the GPU), `vi` (the VI filter, or reading the GPU's picture back), `idle` (waiting for the TV: spare time). Also logged every 10 s in `boot.log` |
| Volume | the game's sound, 0–100% (default 100%) |
| Sound, Download covers | |
| Check for updates | ask at start-up when a newer GitHub release exists (see [Updates](#updates)) |
@@ -243,7 +256,7 @@ At every start, before it asks for `/data` (the console's HTTPS works only then,
Interpreter.
- **The RSP is the next bottleneck.** cxd4 interprets every graphics microcode instruction (GoldenEye:
~30% of the time); audio goes through rsp-hle (Audio processing: Fast).
- **Unsupported:** 64DD, the Transfer Pak, screenshots, a cheats menu, netplay.
- **Unsupported:** 64DD, the Transfer Pak, screenshots, netplay.
- PAL games are paced by the core's millisecond limiter, so motion is less smooth than NTSC.
## Building
@@ -306,8 +319,10 @@ its NASM stubs are translated to GNU syntax in `n64/core/linkage_x64.S`, and the
generated from the core's `asm_defines.c` with its own `tools/gen_asm_script.sh`. On the host,
`N64PS5_JIT=direct` exercises the PS5's direct-memory way (an anonymous RWX mapping stands in for it).
**Art.** `tools/make_art.py app/sce_sys` redraws `icon0.png`, `background-source.png` and the BC7
`pic0.dds`/`pic1.dds`.
**Art.** The home screen's icon and background are pictures in `app/art/` (`icon.png`, `background.png`).
`tools/make_art.py app/sce_sys --images app/art/icon.png app/art/background.png` fits them to `icon0.png`
(512x512) and `background-source.png` (3840x2160) and writes the BC7 `pic0.dds`/`pic1.dds`; without
`--images` it draws the old art itself.
## Source layout
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 KiB

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 314 KiB

+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "99064",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA99064_00-MUPEN64PS5000001",
"contentVersion": "00.007.003",
"contentVersion": "00.007.004",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
Binary file not shown.
Binary file not shown.
+186
View File
@@ -0,0 +1,186 @@
// Mupen64Plus PS5 frontend: the cheat database (fe_cheats.h).
//
// mupencheat.txt, as the core ships it (converted from Project64's database by Gent):
//
// crc 635A2BFF-8B022326-C:45 the ROM header's CRC1, CRC2 and country code
// gn Super Mario 64 (U)
// cn Infinite Lives a cheat (a '\' puts it in a group)
// cd Some explanation optional
// 8033B21D 0064 its codes: address, value
// 8013CD0E ???? 0001:"1 Lap",... a value to choose from a list
//
// SPDX-License-Identifier: MIT
#include "fe_cheats.h"
#include "OrbisPaths.h"
#include "zlib.h"
#include <cstdlib>
#include <cstring>
#ifndef CHEATS_Z
#error "CHEATS_Z: the compressed mupencheat.txt (the Makefile builds it)"
#endif
#define FE_INCBIN(sym, path) \
__asm__(".section .rodata\n" \
".balign 16\n" \
".global " #sym "_begin\n" #sym "_begin:\n" \
".incbin \"" path "\"\n" \
".global " #sym "_end\n" #sym "_end:\n" \
".previous\n"); \
extern "C" const unsigned char sym##_begin[]; \
extern "C" const unsigned char sym##_end[];
// 4 bytes: the text's size (little endian), then the text, zlib-compressed
FE_INCBIN(n64ps5_cheats_z, CHEATS_Z)
namespace fe
{
namespace cheats
{
namespace
{
std::string Inflate()
{
const unsigned char* z = n64ps5_cheats_z_begin;
const size_t zsize = size_t(n64ps5_cheats_z_end - n64ps5_cheats_z_begin);
if (zsize < 4)
return "";
uLongf size = uLongf(z[0]) | uLongf(z[1]) << 8 | uLongf(z[2]) << 16 | uLongf(z[3]) << 24;
std::string text(size, '\0');
if (uncompress(reinterpret_cast<Bytef*>(&text[0]), &size, z + 4, uLong(zsize - 4)) != Z_OK)
{
OrbisLog("[cheats] the built-in cheat list is damaged");
return "";
}
text.resize(size);
return text;
}
// a code line: "8033B21D 0064" or "8013CD0E ???? 0001:"1 Lap",0002:"2 Laps""
bool ParseCode(const std::string& line, Cheat& c)
{
char* end = nullptr;
const unsigned long address = strtoul(line.c_str(), &end, 16);
if (end == line.c_str() || *end != ' ')
return false;
const std::string rest = end + 1;
if (rest.compare(0, 4, "????") == 0)
{
c.addresses.push_back(uint32_t(address));
c.values.push_back(-1);
// the choices: hex value, ':', quoted name, separated by commas. Every "????" of a cheat takes the same
// choice; the list is on one of its lines (28 cheats leave the first one's empty).
std::vector<std::pair<std::string, int>> options;
size_t at = 4;
while (at < rest.size())
{
while (at < rest.size() && (rest[at] == ' ' || rest[at] == ','))
at++;
const size_t colon = rest.find(':', at);
const size_t q1 = colon == std::string::npos ? colon : rest.find('"', colon);
const size_t q2 = q1 == std::string::npos ? q1 : rest.find('"', q1 + 1);
if (q2 == std::string::npos)
break;
const int value = int(strtol(rest.substr(at, colon - at).c_str(), nullptr, 16));
options.emplace_back(rest.substr(q1 + 1, q2 - q1 - 1), value);
at = q2 + 1;
}
if (c.options.empty())
c.options = std::move(options);
return true;
}
c.addresses.push_back(uint32_t(address));
c.values.push_back(int(strtol(rest.c_str(), nullptr, 16)));
return true;
}
// codes the core applies only while the GameShark's button is held
bool NeedsGameSharkButton(const Cheat& c)
{
for (uint32_t a : c.addresses)
{
const uint32_t type = a & 0xFF000000u;
if (type == 0x88000000u || type == 0x89000000u || type == 0xA8000000u || type == 0xA9000000u ||
(type >= 0xD8000000u && type <= 0xDB000000u))
return true;
}
return false;
}
// a cheat the menu can offer: it has codes, needs no GameShark button, and a choice has its list
bool Usable(const Cheat& c)
{
if (c.addresses.empty() || NeedsGameSharkButton(c))
return false;
for (int v : c.values)
if (v < 0 && c.options.empty())
return false;
return true;
}
} // namespace
std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::string* game)
{
std::vector<Cheat> out;
const std::string text = Inflate();
char want[48];
snprintf(want, sizeof(want), "crc %08X-%08X-C:%02X", unsigned(crc1), unsigned(crc2), unsigned(country));
size_t at = text.find(want);
if (at == std::string::npos)
return out;
at = text.find('\n', at);
int skipped = 0;
Cheat* cur = nullptr;
while (at != std::string::npos && at + 1 < text.size())
{
const size_t start = at + 1;
at = text.find('\n', start);
std::string line = text.substr(start, at == std::string::npos ? std::string::npos : at - start);
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (line.compare(0, 4, "crc ") == 0)
break; // the next game
if (line.compare(0, 3, "gn ") == 0)
{
if (game)
*game = line.substr(3);
}
else if (line.compare(0, 4, " cn ") == 0)
{
if (cur && !Usable(*cur))
{
skipped++;
out.pop_back();
}
out.emplace_back();
cur = &out.back();
cur->name = line.substr(4);
}
else if (cur && line.compare(0, 5, " cd ") == 0)
cur->note = line.substr(5);
else if (cur && line.size() > 2 && line[0] == ' ' && line[1] == ' ')
ParseCode(line.substr(2), *cur);
}
if (cur && !Usable(*cur))
{
skipped++;
out.pop_back();
}
if (skipped)
OrbisLog("[cheats] %d cheat(s) left out (the GameShark's button, or no codes)", skipped);
return out;
}
std::string DisplayName(const std::string& name)
{
std::string s = name;
for (size_t i = s.find('\\'); i != std::string::npos; i = s.find('\\', i + 2))
s.replace(i, 1, ": ");
return s;
}
} // namespace cheats
} // namespace fe
+34
View File
@@ -0,0 +1,34 @@
// Mupen64Plus PS5 frontend: cheats -- the GameShark codes of mupen64plus-core's data/mupencheat.txt (built into
// eboot.bin, compressed), switched on and off in the pause menu and remembered per game (fe_emu.cpp).
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace fe
{
namespace cheats
{
struct Cheat
{
std::string name; // as in the file: "Play As\Player 1" is "Player 1" in the group "Play As"
std::string note; // what to know before using it (its "cd" line), often empty
std::vector<uint32_t> addresses;
std::vector<int> values; // -1: the chosen option's value
std::vector<std::pair<std::string, int>> options; // a cheat with choices ("????"), else empty
int state = 0; // 0 off; then 1 on, or with options 1 + the chosen option's index
int States() const { return options.empty() ? 2 : int(options.size()) + 1; }
};
// The cheats of the game whose ROM header has these CRCs and country code (the database's "crc" lines), in
// the database's order; `game` gets its name there. Cheats that need the GameShark's button are left out:
// the DualSense has none.
std::vector<Cheat> ForGame(uint32_t crc1, uint32_t crc2, uint8_t country, std::string* game);
// "Play As\Player 1" -> "Play As: Player 1"
std::string DisplayName(const std::string& name);
} // namespace cheats
} // namespace fe
+299
View File
@@ -0,0 +1,299 @@
// Mupen64Plus PS5 frontend: anti-aliased shapes (fe_draw.h). Rounded rectangles and circles use their signed
// distance (coverage = 0.5 - distance), the triangles 4 x 4 samples per pixel: menus only, a few per frame.
// SPDX-License-Identifier: MIT
#include "fe_draw.h"
#include "ProsperoVideo.h"
#include <algorithm>
#include <cmath>
namespace fe
{
namespace gfx
{
namespace
{
constexpr int W = ps5video::kWidth;
constexpr int H = ps5video::kHeight;
inline float Clamp01(float v)
{
return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v);
}
inline void Put(int x, int y, uint32_t color, float coverage, int alpha)
{
if (coverage <= 0.0f || x < 0 || y < 0 || x >= W || y >= H)
return;
uint32_t& p = ps5video::Surface()[size_t(y) * W + x];
p = Blend(p, color, int(coverage * alpha + 0.5f));
}
struct RoundRect
{
float cx, cy, hw, hh, r;
RoundRect(int x, int y, int w, int h, int radius)
{
hw = w * 0.5f;
hh = h * 0.5f;
cx = x + hw;
cy = y + hh;
r = std::max(0.0f, std::min(float(radius), std::min(hw, hh)));
}
// signed distance of a pixel's centre to the outline (negative inside)
float Distance(int x, int y) const
{
const float ax = std::fabs(x + 0.5f - cx), ay = std::fabs(y + 0.5f - cy);
const float qx = ax - (hw - r), qy = ay - (hh - r);
if (qx <= 0.0f || qy <= 0.0f)
return std::max(ax - hw, ay - hh);
return std::sqrt(qx * qx + qy * qy) - r;
}
// deep inside, away from the edges and corners by more than `band`
bool Inside(int x, int y, float band) const
{
const float ax = std::fabs(x + 0.5f - cx), ay = std::fabs(y + 0.5f - cy);
return (ax < hw - r && ay < hh - band) || (ay < hh - r && ax < hw - band);
}
};
} // namespace
uint32_t Blend(uint32_t dst, uint32_t color, int alpha)
{
if (alpha <= 0)
return dst;
const uint32_t a = uint32_t(std::min(alpha, 255));
const uint32_t w = a + (a >> 7);
const uint32_t iw = 256 - w;
const uint32_t rb = (((dst & 0xff00ffu) * iw + (color & 0xff00ffu) * w) >> 8) & 0xff00ffu;
const uint32_t g = (((dst & 0x00ff00u) * iw + (color & 0x00ff00u) * w) >> 8) & 0x00ff00u;
return 0xff000000u | rb | g;
}
uint32_t Mix(uint32_t a, uint32_t b, float t)
{
return Blend(a, b, int(Clamp01(t) * 255.0f + 0.5f));
}
void FillRoundRect(int x, int y, int w, int h, int r, uint32_t color, int alpha)
{
const RoundRect rr(x, y, w, h, r);
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
Put(xx, yy, color, rr.Inside(xx, yy, 1.0f) ? 1.0f : Clamp01(0.5f - rr.Distance(xx, yy)), alpha);
}
void GradientRoundRect(int x, int y, int w, int h, int r, uint32_t c0, uint32_t c1, bool horizontal, int alpha)
{
const RoundRect rr(x, y, w, h, r);
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
{
const float t = horizontal ? float(xx - x) / std::max(1, w - 1) : float(yy - y) / std::max(1, h - 1);
Put(xx, yy, Mix(c0, c1, t), rr.Inside(xx, yy, 1.0f) ? 1.0f : Clamp01(0.5f - rr.Distance(xx, yy)), alpha);
}
}
void StrokeRoundRect(int x, int y, int w, int h, int r, float thick, uint32_t color, int alpha)
{
const RoundRect rr(x, y, w, h, r);
for (int yy = std::max(0, y); yy < std::min(H, y + h); yy++)
for (int xx = std::max(0, x); xx < std::min(W, x + w); xx++)
{
if (rr.Inside(xx, yy, thick + 1.0f))
continue;
const float d = rr.Distance(xx, yy);
Put(xx, yy, color, Clamp01(0.5f - d) - Clamp01(0.5f - d - thick), alpha);
}
}
void GlowRoundRect(int x, int y, int w, int h, int r, int size, uint32_t color, int alpha)
{
const RoundRect rr(x, y, w, h, r);
for (int yy = std::max(0, y - size); yy < std::min(H, y + h + size); yy++)
for (int xx = std::max(0, x - size); xx < std::min(W, x + w + size); xx++)
{
if (rr.Inside(xx, yy, 0.0f))
continue;
const float d = rr.Distance(xx, yy);
if (d <= 0.0f || d >= size)
continue;
const float f = 1.0f - d / size;
Put(xx, yy, color, f * f, alpha);
}
}
void FillCircle(float cx, float cy, float r, uint32_t color, int alpha)
{
for (int yy = std::max(0, int(cy - r - 1)); yy <= std::min(H - 1, int(cy + r + 1)); yy++)
for (int xx = std::max(0, int(cx - r - 1)); xx <= std::min(W - 1, int(cx + r + 1)); xx++)
{
const float dx = xx + 0.5f - cx, dy = yy + 0.5f - cy;
Put(xx, yy, color, Clamp01(0.5f - (std::sqrt(dx * dx + dy * dy) - r)), alpha);
}
}
void StrokeCircle(float cx, float cy, float r, float thick, uint32_t color, int alpha)
{
StrokeArc(cx, cy, r - thick * 0.5f, thick, 0.0f, 360.0f, color, alpha);
}
void FillTriangle(float x0, float y0, float x1, float y1, float x2, float y2, uint32_t color, int alpha)
{
const float area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0);
if (std::fabs(area) < 1e-3f)
return;
const float s = area > 0 ? 1.0f : -1.0f;
auto edge = [s](float ax, float ay, float bx, float by, float px, float py) {
return s * ((bx - ax) * (py - ay) - (by - ay) * (px - ax));
};
const int minx = std::max(0, int(std::floor(std::min({x0, x1, x2})))), maxx = std::min(W - 1, int(std::ceil(std::max({x0, x1, x2}))));
const int miny = std::max(0, int(std::floor(std::min({y0, y1, y2})))), maxy = std::min(H - 1, int(std::ceil(std::max({y0, y1, y2}))));
for (int yy = miny; yy <= maxy; yy++)
for (int xx = minx; xx <= maxx; xx++)
{
int in = 0;
for (int sy = 0; sy < 4; sy++)
for (int sx = 0; sx < 4; sx++)
{
const float px = xx + (sx + 0.5f) / 4.0f, py = yy + (sy + 0.5f) / 4.0f;
if (edge(x0, y0, x1, y1, px, py) >= 0 && edge(x1, y1, x2, y2, px, py) >= 0 && edge(x2, y2, x0, y0, px, py) >= 0)
in++;
}
Put(xx, yy, color, in / 16.0f, alpha);
}
}
void StrokeArc(float cx, float cy, float r, float thick, float a0, float a1, uint32_t color, int alpha)
{
const float outer = r + thick * 0.5f + 1.0f;
const bool full = a1 - a0 >= 360.0f;
for (int yy = std::max(0, int(cy - outer)); yy <= std::min(H - 1, int(cy + outer)); yy++)
for (int xx = std::max(0, int(cx - outer)); xx <= std::min(W - 1, int(cx + outer)); xx++)
{
const float dx = xx + 0.5f - cx, dy = yy + 0.5f - cy;
const float cov = Clamp01(thick * 0.5f + 0.5f - std::fabs(std::sqrt(dx * dx + dy * dy) - r));
if (cov <= 0.0f)
continue;
if (!full)
{
float a = std::atan2(dy, dx) * 57.29578f;
if (a < 0)
a += 360.0f;
float lo = std::fmod(a0, 360.0f), span = a1 - a0;
if (lo < 0)
lo += 360.0f;
float rel = a - lo;
if (rel < 0)
rel += 360.0f;
if (rel > span)
continue;
}
Put(xx, yy, color, cov, alpha);
}
}
void HLine(int x, int y, int w, uint32_t color, int alpha)
{
for (int xx = x; xx < x + w; xx++)
Put(xx, y, color, 1.0f, alpha);
}
void VLine(int x, int y, int h, uint32_t color, int alpha)
{
for (int yy = y; yy < y + h; yy++)
Put(x, yy, color, 1.0f, alpha);
}
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color)
{
const float s = float(size);
const float cx = x + s * 0.5f, cy = y + s * 0.5f;
switch (icon)
{
case Icon::Pause:
{
const int bw = int(s * 0.2f), bh = int(s * 0.62f), gap = int(s * 0.16f);
const int top = int(cy - bh * 0.5f), left = int(cx - gap * 0.5f) - bw;
FillRoundRect(left, top, bw, bh, bw / 3, color);
FillRoundRect(int(cx + gap * 0.5f), top, bw, bh, bw / 3, color);
break;
}
case Icon::Play:
FillTriangle(x + s * 0.22f, y + s * 0.14f, x + s * 0.22f, y + s * 0.86f, x + s * 0.86f, cy, color);
break;
case Icon::Restart:
{
// most of a circle, open at the top, with an arrowhead at its upper left end pointing on round
const float r = s * 0.32f, t = s * 0.12f;
StrokeArc(cx, cy, r, t, 300.0f, 600.0f, color);
const float a = 240.0f / 57.29578f;
const float px = cx + r * std::cos(a), py = cy + r * std::sin(a);
const float tx = -std::sin(a), ty = std::cos(a); // the way round (clockwise)
const float nx = std::cos(a), ny = std::sin(a);
const float hl = s * 0.22f, hw = s * 0.17f;
FillTriangle(px + tx * hl, py + ty * hl, px + nx * hw, py + ny * hw, px - nx * hw, py - ny * hw, color);
break;
}
case Icon::FastForward:
FillTriangle(x + s * 0.08f, y + s * 0.2f, x + s * 0.08f, y + s * 0.8f, x + s * 0.5f, cy, color);
FillTriangle(x + s * 0.5f, y + s * 0.2f, x + s * 0.5f, y + s * 0.8f, x + s * 0.92f, cy, color);
break;
case Icon::Grid:
for (int j = 0; j < 3; j++)
for (int i = 0; i < 3; i++)
FillRoundRect(x + int(s * (0.1f + 0.29f * i)), y + int(s * (0.1f + 0.29f * j)), int(s * 0.22f),
int(s * 0.22f), 2, color);
break;
case Icon::Tiles:
for (int j = 0; j < 2; j++)
for (int i = 0; i < 2; i++)
FillRoundRect(x + int(s * (0.1f + 0.43f * i)), y + int(s * (0.1f + 0.43f * j)), int(s * 0.36f),
int(s * 0.36f), int(s * 0.08f), color);
break;
case Icon::Frame:
{
// a dashed square: four dashes a side
const int a = int(s * 0.12f), b = int(s * 0.88f), t = std::max(2, int(s * 0.09f)), d = (b - a) / 7;
for (int k = 0; k < 7; k += 2)
{
FillRoundRect(x + a + k * d, y + a, d, t, 1, color);
FillRoundRect(x + a + k * d, y + b - t, d, t, 1, color);
FillRoundRect(x + a, y + a + k * d, t, d, 1, color);
FillRoundRect(x + b - t, y + a + k * d, t, d, 1, color);
}
break;
}
case Icon::Bars:
for (int i = 0; i < 3; i++)
{
const float hgt = s * (0.3f + 0.25f * i);
FillRoundRect(x + int(s * (0.12f + 0.28f * i)), y + int(s * 0.88f - hgt), int(s * 0.2f), int(hgt), 2, color);
}
break;
case Icon::Screen:
StrokeRoundRect(x + int(s * 0.06f), y + int(s * 0.2f), int(s * 0.88f), int(s * 0.6f), int(s * 0.08f),
std::max(2.0f, s * 0.09f), color);
break;
case Icon::Gauge:
{
const float r = s * 0.38f;
StrokeArc(cx, cy + s * 0.08f, r, s * 0.1f, 150.0f, 390.0f, color);
const float a = 300.0f / 57.29578f;
const float ex = cx + r * 0.8f * std::cos(a), ey = cy + s * 0.08f + r * 0.8f * std::sin(a);
const float nx = -std::sin(a) * s * 0.05f, ny = std::cos(a) * s * 0.05f;
FillTriangle(cx + nx, cy + s * 0.08f + ny, cx - nx, cy + s * 0.08f - ny, ex, ey, color);
FillCircle(cx, cy + s * 0.08f, s * 0.08f, color);
break;
}
case Icon::Download:
FillRoundRect(int(cx - s * 0.07f), y + int(s * 0.1f), int(s * 0.14f), int(s * 0.4f), 1, color);
FillTriangle(cx - s * 0.25f, y + s * 0.45f, cx + s * 0.25f, y + s * 0.45f, cx, y + s * 0.72f, color);
FillRoundRect(x + int(s * 0.12f), y + int(s * 0.8f), int(s * 0.76f), std::max(2, int(s * 0.09f)), 1, color);
break;
}
}
} // namespace gfx
} // namespace fe
+48
View File
@@ -0,0 +1,48 @@
// Mupen64Plus PS5 frontend: anti-aliased shapes on the CPU-drawn screen (ps5video::Surface()), for the pause menu's
// look: rounded panels and buttons, glows, gradients, switches, sliders and a few drawn icons.
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
namespace fe
{
namespace gfx
{
// color over the pixel at `alpha` (0..255)
uint32_t Blend(uint32_t dst, uint32_t color, int alpha);
uint32_t Mix(uint32_t a, uint32_t b, float t); // a -> b
void FillRoundRect(int x, int y, int w, int h, int r, uint32_t color, int alpha = 255);
// from c0 to c1, left to right (horizontal) or top to bottom
void GradientRoundRect(int x, int y, int w, int h, int r, uint32_t c0, uint32_t c1, bool horizontal, int alpha = 255);
// a border `thick` pixels wide, inside the rectangle
void StrokeRoundRect(int x, int y, int w, int h, int r, float thick, uint32_t color, int alpha = 255);
// light around the rectangle, fading out over `size` pixels
void GlowRoundRect(int x, int y, int w, int h, int r, int size, uint32_t color, int alpha);
void FillCircle(float cx, float cy, float r, uint32_t color, int alpha = 255);
void StrokeCircle(float cx, float cy, float r, float thick, uint32_t color, int alpha = 255);
void FillTriangle(float x0, float y0, float x1, float y1, float x2, float y2, uint32_t color, int alpha = 255);
// part of a ring, angles in degrees, clockwise from 3 o'clock (screen y grows downwards)
void StrokeArc(float cx, float cy, float r, float thick, float a0, float a1, uint32_t color, int alpha = 255);
void HLine(int x, int y, int w, uint32_t color, int alpha = 255);
void VLine(int x, int y, int h, uint32_t color, int alpha = 255);
// small icons drawn with the shapes above, in a `size` x `size` box
enum class Icon
{
Pause,
Play,
Restart, // a circular arrow
FastForward,
Grid, // 3 x 3 squares
Tiles, // 2 x 2 squares
Frame, // a dashed square
Bars, // a bar chart
Screen, // a rectangle
Gauge,
Download,
};
void DrawIcon(Icon icon, int x, int y, int size, uint32_t color);
} // namespace gfx
} // namespace fe
+160 -6
View File
@@ -17,6 +17,8 @@
#include "fe_emu.h"
#include "fe_cheats.h"
#include "fe_games.h"
#include "fe_menu.h"
#include "fe_settings.h"
@@ -49,6 +51,7 @@
#include <cstdarg>
#include <dirent.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <mutex>
@@ -221,6 +224,13 @@ struct State
uint32_t prev_buttons = 0;
bool block_until_release = false;
int pending_save_slot = -1;
// the controller layout while a game is loaded (from RunGame on): its own or Settings'
bool game_layout = false;
bool own_layout = false;
fe::Layout layout;
// cheats: the database's for this ROM, with what the player chose
std::vector<fe::cheats::Cheat> cheats;
std::string cheats_game;
// "continue where you left off": the state loaded at the first VI, and the one saved on the way out
bool resume_load = false;
bool resume_loading = false;
@@ -483,6 +493,72 @@ std::string FileBase(const std::string& path)
return base;
}
// The cheats the player left on, by name: "<state>\t<name>" lines (fe_cheats.h's state).
std::string CheatsPath()
{
return OrbisDir("cheats") + "/" + g.file_base + ".txt";
}
void LoadCheatStates()
{
FILE* f = fopen(CheatsPath().c_str(), "r");
if (!f)
return;
char line[512];
while (fgets(line, sizeof(line), f))
{
line[strcspn(line, "\r\n")] = 0;
char* tab = strchr(line, '\t');
if (!tab)
continue;
*tab = 0;
const int state = atoi(line);
for (fe::cheats::Cheat& c : g.cheats)
if (c.name == tab + 1 && state > 0 && state < c.States())
c.state = state;
}
fclose(f);
}
void SaveCheatStates()
{
std::string text;
for (const fe::cheats::Cheat& c : g.cheats)
if (c.state)
text += std::to_string(c.state) + "\t" + c.name + "\n";
if (text.empty())
{
unlink(CheatsPath().c_str());
return;
}
OrbisMkdirs(OrbisDir("cheats"));
if (FILE* f = fopen(CheatsPath().c_str(), "w"))
{
fputs(text.c_str(), f);
fclose(f);
}
}
// Hands cheat `index` to the core as it is now: on (with its codes, the chosen option's value in place of
// "????") or off (the core puts the old values back). Its name in the core is unique: "<index>:<name>".
void ApplyCheat(int index)
{
const fe::cheats::Cheat& c = g.cheats[size_t(index)];
const std::string id = std::to_string(index) + ":" + c.name;
if (c.state == 0)
{
CoreCheatEnabled(id.c_str(), 0);
return;
}
const int choice = c.options.empty() ? 0 : c.options[size_t(c.state - 1)].second;
std::vector<m64p_cheat_code> codes;
for (size_t i = 0; i < c.addresses.size(); i++)
codes.push_back({c.addresses[i], c.values[i] < 0 ? choice : c.values[i]});
const m64p_error r = CoreAddCheat(id.c_str(), codes.data(), int(codes.size()));
if (r != M64ERR_SUCCESS)
OrbisLog("[cheats] the core refused \"%s\" (%d)", c.name.c_str(), int(r));
}
// "Continue where you left off": saved when the player leaves the game from the pause menu.
std::string ResumePath(const std::string& file_base)
{
@@ -627,12 +703,19 @@ extern "C" void n64ps5_on_vi(const n64ps5_frame* frame)
n64ps5_input_block(false);
}
const uint32_t combo = SCE_PAD_BUTTON_L3 | SCE_PAD_BUTTON_R3;
const bool menu = (pressed & SCE_PAD_BUTTON_TOUCH_PAD) || ((raw & combo) == combo && (pressed & combo));
if (menu && !g.stopping && !g.leaving)
// the touchpad opens the pause menu; R3 (pressed) saves to the state slot, L3 loads it
if (!g.stopping && !g.leaving)
{
OpenPauseMenu();
g.prev_buttons = ps5input::Pad(0).raw_buttons;
const int slot = fe::Config().state_slot;
if (pressed & SCE_PAD_BUTTON_TOUCH_PAD)
{
OpenPauseMenu();
g.prev_buttons = ps5input::Pad(0).raw_buttons;
}
else if (pressed & SCE_PAD_BUTTON_R3)
emu::Osd(emu::SaveState(slot) ? "Saved in slot %d" : "Could not save in slot %d", slot);
else if (pressed & SCE_PAD_BUTTON_L3)
emu::Osd(emu::StateExists(slot) && emu::LoadState(slot) ? "Loaded slot %d" : "Slot %d is empty", slot);
}
UpdateFps();
@@ -737,6 +820,7 @@ void DeinitCore()
ExitReason RunGame(const std::string& path, std::string* error, bool resume)
{
g.game_layout = false; // Settings' until this game's is known
std::string dummy;
if (!error)
error = &dummy;
@@ -773,6 +857,10 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
g.rom_path = path;
g.file_base = FileBase(path);
g.own_layout = fe::LoadGameLayout(g.file_base, g.layout);
if (!g.own_layout)
g.layout = fe::Config().layout;
g.game_layout = true;
g.pal = IsPalCountry(header.Country_code);
// the title: as on the shelf (No-Intro name), else the catalog's GoodName, else the file name
uint32_t crc = 0;
@@ -802,6 +890,23 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
n64ps5_rsp_set_hle(cfg.hle_audio);
static const char* const kPaks[] = {"none", "Controller Pak", "Rumble Pak"};
OrbisLog("[emu] controller pak: %s", kPaks[cfg.pak < 0 || cfg.pak > 2 ? 1 : cfg.pak]);
// cheats: the database's for this ROM (by the header's CRCs and country), the ones left on are on again
g.cheats_game.clear();
g.cheats = fe::cheats::ForGame(__builtin_bswap32(header.CRC1), __builtin_bswap32(header.CRC2),
header.Country_code, &g.cheats_game);
LoadCheatStates();
std::string cheats_on;
for (size_t i = 0; i < g.cheats.size(); i++)
if (g.cheats[i].state)
{
ApplyCheat(int(i));
cheats_on += (cheats_on.empty() ? ": " : ", ") + fe::cheats::DisplayName(g.cheats[i].name);
}
if (!g.cheats.empty())
OrbisLog("[cheats] %zu for \"%s\"%s", g.cheats.size(), g.cheats_game.c_str(),
cheats_on.empty() ? ", none on" : cheats_on.c_str());
OrbisLog("[emu] controller layout: %s%s", g.own_layout ? "this game's own " : "",
g.layout == fe::Layout() ? "(the N64 pad's)" : fe::LayoutToText(g.layout).c_str());
LogSaves("at start", settings.goodname, settings.MD5);
const n64ps5_gfx_options gfx = {cfg.vi_filter ? 0 : 1, cfg.hide_overscan, cfg.render_threads, cfg.dp_compat,
cfg.upscale, cfg.gpu_sync};
@@ -878,6 +983,7 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume)
n64ps5_audio_set_mute(!fe::Config().audio);
g.leaving = false;
g.running = false;
g.game_layout = false;
N64_STAGE(Emu, "idle");
if (r != M64ERR_SUCCESS && g.vi == 0)
{
@@ -907,6 +1013,48 @@ bool GameLoaded()
return g.running;
}
std::vector<fe::cheats::Cheat>& Cheats()
{
return g.cheats;
}
std::string CheatsGameName()
{
return g.cheats_game;
}
void SetCheat(int index, int state)
{
if (!g.running || index < 0 || index >= int(g.cheats.size()))
return;
fe::cheats::Cheat& c = g.cheats[size_t(index)];
c.state = state < 0 || state >= c.States() ? 0 : state;
ApplyCheat(index);
SaveCheatStates();
OrbisLog("[cheats] %s: %s", fe::cheats::DisplayName(c.name).c_str(),
c.state == 0 ? "off" : c.options.empty() ? "on" : c.options[size_t(c.state - 1)].first.c_str());
}
fe::Layout GameLayout()
{
return g.game_layout ? g.layout : fe::Config().layout;
}
bool GameHasOwnLayout()
{
return g.game_layout && g.own_layout;
}
void SetGameLayout(const fe::Layout& layout)
{
if (!g.game_layout)
return;
g.layout = layout;
g.own_layout = layout != fe::Config().layout;
fe::SaveGameLayout(g.file_base, g.own_layout ? &layout : nullptr);
ApplySettings();
}
std::string GameName()
{
return g.title;
@@ -916,8 +1064,14 @@ void ApplySettings()
{
const fe::Settings& cfg = fe::Config();
n64ps5_audio_set_mute(!cfg.audio);
n64ps5_audio_set_volume(cfg.volume);
n64ps5_audio_set_fast_forward(g.fast_forward.load());
const n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak, cfg.invert_stick, cfg.invert_c_stick};
n64ps5_input_options in = {cfg.deadzone, 80, cfg.pak, cfg.invert_stick, cfg.invert_c_stick, {}};
const fe::Layout& layout = g.game_layout ? g.layout : cfg.layout;
memcpy(in.layout, layout.to, sizeof(in.layout));
// L3 and R3 load and save states (n64ps5_on_vi): the game doesn't get them
in.layout[N64PS5_PAD_L3] = N64PS5_N64_NONE;
in.layout[N64PS5_PAD_R3] = N64PS5_N64_NONE;
n64ps5_input_set_options(&in);
ps5video::InvalidateN64();
}
+13
View File
@@ -2,6 +2,9 @@
// SPDX-License-Identifier: MIT
#pragma once
#include "fe_cheats.h"
#include "fe_settings.h"
#include <string>
namespace emu
@@ -24,6 +27,16 @@ ExitReason RunGame(const std::string& path, std::string* error, bool resume = fa
bool ResumeStateExists(const std::string& rom, std::string* saved_at = nullptr);
bool GameLoaded();
// The running game's cheats (fe_cheats.h; empty when the database has none for it), and switching one
// (pause menu): applied at once and remembered for the game in cheats/<ROM file>.txt.
std::vector<fe::cheats::Cheat>& Cheats();
std::string CheatsGameName();
void SetCheat(int index, int state);
// The running game's controller layout: its own (fe::LoadGameLayout) or else the one of Settings. Setting it
// (pause menu) gives the game its own layout file, unless it is the same as Settings' again.
fe::Layout GameLayout();
bool GameHasOwnLayout();
void SetGameLayout(const fe::Layout& layout);
std::string GameName(); // the title shown on the shelf (No-Intro name, ROM header name or file name)
// Settings from fe::Config() that apply while a game runs (aspect, smoothing, FPS, sound, dead zone).
+836 -136
View File
File diff suppressed because it is too large. Load diff
+96
View File
@@ -8,6 +8,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
namespace fe
{
@@ -22,8 +23,97 @@ int Clamp(int v, int lo, int hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
// the layout's names in the settings file and the per-game files
const char* const kPadKeys[N64PS5_PAD_COUNT] = {"cross", "circle", "square", "triangle", "l1", "r1", "l2", "r2",
"l3", "r3", "options", "up", "down", "left", "right", "rs_up", "rs_down", "rs_left", "rs_right"};
const char* const kN64Keys[N64PS5_N64_COUNT] = {"none", "a", "b", "z", "l", "r", "start", "c_up", "c_down",
"c_left", "c_right", "d_up", "d_down", "d_left", "d_right"};
std::string GameLayoutPath(const std::string& file_base)
{
return OrbisDir("layouts") + "/" + file_base + ".txt";
}
} // namespace
Layout::Layout()
{
n64ps5_input_default_layout(to);
}
bool Layout::operator==(const Layout& o) const
{
return memcmp(to, o.to, sizeof(to)) == 0;
}
std::string LayoutToText(const Layout& layout)
{
std::string s;
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
{
if (i)
s += ',';
s += std::string(kPadKeys[i]) + '=' + kN64Keys[layout.to[i] < N64PS5_N64_COUNT ? layout.to[i] : 0];
}
return s;
}
Layout LayoutFromText(const std::string& text)
{
Layout layout;
size_t at = 0;
while (at < text.size())
{
size_t end = text.find(',', at);
if (end == std::string::npos)
end = text.size();
const std::string item = text.substr(at, end - at);
at = end + 1;
const size_t eq = item.find('=');
if (eq == std::string::npos)
continue;
const std::string pad = item.substr(0, eq), n64 = item.substr(eq + 1);
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
if (pad == kPadKeys[i])
for (int t = 0; t < N64PS5_N64_COUNT; t++)
if (n64 == kN64Keys[t])
layout.to[i] = (unsigned char)t;
}
return layout;
}
bool LoadGameLayout(const std::string& file_base, Layout& out)
{
FILE* f = fopen(GameLayoutPath(file_base).c_str(), "r");
if (!f)
return false;
char line[1024] = "";
const bool ok = fgets(line, sizeof(line), f) != nullptr;
fclose(f);
if (!ok)
return false;
line[strcspn(line, "\r\n")] = 0;
out = LayoutFromText(line);
return true;
}
void SaveGameLayout(const std::string& file_base, const Layout* layout)
{
const std::string path = GameLayoutPath(file_base);
if (!layout)
{
unlink(path.c_str());
return;
}
OrbisMkdirs(OrbisDir("layouts"));
if (FILE* f = fopen(path.c_str(), "w"))
{
fprintf(f, "%s\n", LayoutToText(*layout).c_str());
fclose(f);
OrbisLog("[settings] %s: own controller layout", path.c_str());
}
}
Settings& Config()
{
static Settings s;
@@ -76,6 +166,8 @@ void Settings::Load()
dp_compat = Clamp(atoi(val), 0, 2);
else if (key == "audio")
audio = atoi(val) != 0;
else if (key == "volume")
volume = Clamp(atoi(val), 0, 100);
else if (key == "pak")
pak = Clamp(atoi(val), 0, 2);
else if (key == "deadzone")
@@ -84,6 +176,8 @@ void Settings::Load()
invert_stick = Clamp(atoi(val), 0, 3);
else if (key == "invert_c_stick")
invert_c_stick = Clamp(atoi(val), 0, 3);
else if (key == "layout")
layout = LayoutFromText(val);
else if (key == "game_view")
game_view = Clamp(atoi(val), 0, 2);
else if (key == "state_slot")
@@ -127,10 +221,12 @@ void Settings::Save() const
fprintf(f, "gpu_sync=%d\n", gpu_sync ? 1 : 0);
fprintf(f, "dp_compat=%d\n", dp_compat);
fprintf(f, "audio=%d\n", audio ? 1 : 0);
fprintf(f, "volume=%d\n", volume);
fprintf(f, "pak=%d\n", pak);
fprintf(f, "deadzone=%d\n", deadzone);
fprintf(f, "invert_stick=%d\n", invert_stick);
fprintf(f, "invert_c_stick=%d\n", invert_c_stick);
fprintf(f, "layout=%s\n", LayoutToText(layout).c_str());
fprintf(f, "game_view=%d\n", game_view);
fprintf(f, "state_slot=%d\n", state_slot);
fprintf(f, "resume=%d\n", resume ? 1 : 0);
+21
View File
@@ -2,10 +2,29 @@
// SPDX-License-Identifier: MIT
#pragma once
#include "n64_bridge.h"
#include <string>
namespace fe
{
// The controller layout: for each DualSense control (N64PS5_PAD_*), the N64 button it presses (N64PS5_N64_*).
// The default one is the N64 pad's layout by position (n64ps5_input_default_layout).
struct Layout
{
unsigned char to[N64PS5_PAD_COUNT];
Layout();
bool operator==(const Layout& o) const;
bool operator!=(const Layout& o) const { return !(*this == o); }
};
// "cross=a,circle=c_down,..."; names it doesn't know keep their default
std::string LayoutToText(const Layout& layout);
Layout LayoutFromText(const std::string& text);
// A game's own layout (pause menu): <root>/layouts/<ROM file name>.txt, by the ROM's file name like its states.
bool LoadGameLayout(const std::string& file_base, Layout& out);
// With nullptr the file goes and the game follows the layout of Settings.
void SaveGameLayout(const std::string& file_base, const Layout* layout);
struct Settings
{
// picture
@@ -24,11 +43,13 @@ struct Settings
int render_threads = 6; // angrylion workers (1 = single-threaded)
int dp_compat = 1; // angrylion sync points between its workers: 0 fewest (fastest), 1, 2 most (safest)
bool audio = true;
int volume = 100; // the game's sound, 0..100 %
// controller
int pak = 1; // 0 none, 1 Controller Pak, 2 Rumble Pak
int deadzone = 12; // % of the stick's travel
int invert_stick = 0; // the left (analog) stick's axes turned round: 0 off, 1 X, 2 Y, 3 X and Y
int invert_c_stick = 0; // the right stick (C buttons), the same way
Layout layout; // every game's, unless it has its own (LoadGameLayout)
// misc
int state_slot = 0; // 0..9
bool resume = true; // leaving a game saves where the player was; starting it again offers to continue there
+27 -10
View File
@@ -191,19 +191,19 @@ struct Target
int w, h, pitch;
};
void Draw(const Target& t, int x, int y, const char* utf8, int scale, uint32_t color, bool shadow)
// bold: the text again 1..bold pixels to the right (one weight of Roboto is built in)
void Draw(const Target& t, int x, int y, const char* utf8, int px, uint32_t color, bool shadow, int bold = 0)
{
Fonts& fonts = F();
if (!fonts.ok || !utf8)
return;
const int px = EmPx(scale);
const int baseline = y + int(std::lround(px * 0.80f));
const int sh = std::max(1, scale / 2); // shadow offset
for (int pass = shadow ? 0 : 1; pass < 2; pass++)
const int sh = std::max(1, px / 20); // shadow offset
for (int pass = shadow ? 0 : 1; pass < 2 + bold; pass++)
{
const uint32_t col = pass == 0 ? 0xff000000u : color;
const uint32_t strength = pass == 0 ? 150 : 255;
const int ox = pass == 0 ? sh : 0, oy = pass == 0 ? sh : 0;
const int ox = pass == 0 ? sh : pass - 1, oy = pass == 0 ? sh : 0;
float pen = float(x);
uint32_t prev = 0;
const char* s = utf8;
@@ -238,11 +238,15 @@ void Draw(const Target& t, int x, int y, const char* utf8, int scale, uint32_t c
} // namespace
int TextWidth(const char* utf8, int scale)
{
return TextWidthPx(utf8, EmPx(scale));
}
int TextWidthPx(const char* utf8, int px)
{
Fonts& fonts = F();
if (!fonts.ok || !utf8)
return 0;
const int px = EmPx(scale);
float pen = 0;
uint32_t prev = 0;
const char* s = utf8;
@@ -263,17 +267,30 @@ void DrawText(int x, int y, const char* utf8, int scale, uint32_t color, bool ou
uint32_t* surf = ps5video::Surface();
if (!surf)
return;
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, scale, color, outline);
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, EmPx(scale), color, outline);
}
void DrawTextPx(int x, int y, const char* utf8, int px, uint32_t color, bool outline, int bold)
{
uint32_t* surf = ps5video::Surface();
if (!surf)
return;
Draw(Target{surf, ps5video::kWidth, ps5video::kHeight, ps5video::kWidth}, x, y, utf8, px, color, outline, bold);
}
void DrawTextOn(const Canvas& canvas, int x, int y, const char* utf8, int scale, uint32_t color, bool outline)
{
Draw(Target{canvas.px, canvas.w, canvas.h, canvas.pitch}, x, y, utf8, scale, color, outline);
Draw(Target{canvas.px, canvas.w, canvas.h, canvas.pitch}, x, y, utf8, EmPx(scale), color, outline);
}
std::string FitText(const std::string& utf8, int scale, int max_px)
{
if (TextWidth(utf8.c_str(), scale) <= max_px)
return FitTextPx(utf8, EmPx(scale), max_px);
}
std::string FitTextPx(const std::string& utf8, int px, int max_px)
{
if (TextWidthPx(utf8.c_str(), px) <= max_px)
return utf8;
std::string s = utf8;
while (!s.empty())
@@ -283,7 +300,7 @@ std::string FitText(const std::string& utf8, int scale, int max_px)
n--;
s.resize(n);
const std::string t = s + "\xE2\x80\xA6"; // the ellipsis character, as PS5SX2
if (TextWidth(t.c_str(), scale) <= max_px)
if (TextWidthPx(t.c_str(), px) <= max_px)
return t;
}
return "\xE2\x80\xA6";
+6
View File
@@ -30,6 +30,12 @@ void DrawTextOn(const Canvas& canvas, int x, int y, const char* utf8, int scale,
// Cuts the string so it fits max_px (adds "...").
std::string FitText(const std::string& utf8, int scale, int max_px);
// The same with the line height in pixels (the em; the pause menu's sizes), and a bold made of the text drawn
// again `bold` pixels further right.
int TextWidthPx(const char* utf8, int px);
void DrawTextPx(int x, int y, const char* utf8, int px, uint32_t color, bool outline = false, int bold = 0);
std::string FitTextPx(const std::string& utf8, int px, int max_px);
// PromptFont's controller symbols (the same code points as PS5SX2's fe_text.h).
namespace icon
{
+54 -1
View File
@@ -52,21 +52,74 @@ void n64ps5_rsp_set_hle(bool on);
// ---- audio (plugins/audio_ps5.cpp) ---------------------------------------------------------------------
void n64ps5_audio_set_mute(bool mute);
// Settings "Volume": the game's sound scaled to 0..100 percent.
void n64ps5_audio_set_volume(int percent);
// Fast forward: the audio plugin drops what doesn't fit instead of slowing the game down.
void n64ps5_audio_set_fast_forward(bool on);
int n64ps5_audio_game_rate(void); // the game's current output rate in Hz (0 before it sets one)
int n64ps5_audio_target(void); // the AudioOut ring level the rate control aims at (48 kHz frames)
// ---- input (plugins/input_ps5.cpp) ---------------------------------------------------------------------
// The controller layout (Settings / pause menu "Controller layout"): for each DualSense control, the N64
// button it presses. Several controls may press the same one (L2 and R2 are both Z by default). The left
// stick is always the N64's analog stick.
enum
{
N64PS5_PAD_CROSS,
N64PS5_PAD_CIRCLE,
N64PS5_PAD_SQUARE,
N64PS5_PAD_TRIANGLE,
N64PS5_PAD_L1,
N64PS5_PAD_R1,
N64PS5_PAD_L2,
N64PS5_PAD_R2,
N64PS5_PAD_L3,
N64PS5_PAD_R3,
N64PS5_PAD_OPTIONS,
N64PS5_PAD_UP,
N64PS5_PAD_DOWN,
N64PS5_PAD_LEFT,
N64PS5_PAD_RIGHT,
N64PS5_PAD_RS_UP, // the right stick tilted (half way)
N64PS5_PAD_RS_DOWN,
N64PS5_PAD_RS_LEFT,
N64PS5_PAD_RS_RIGHT,
N64PS5_PAD_COUNT
};
enum
{
N64PS5_N64_NONE,
N64PS5_N64_A,
N64PS5_N64_B,
N64PS5_N64_Z,
N64PS5_N64_L,
N64PS5_N64_R,
N64PS5_N64_START,
N64PS5_N64_C_UP,
N64PS5_N64_C_DOWN,
N64PS5_N64_C_LEFT,
N64PS5_N64_C_RIGHT,
N64PS5_N64_D_UP,
N64PS5_N64_D_DOWN,
N64PS5_N64_D_LEFT,
N64PS5_N64_D_RIGHT,
N64PS5_N64_COUNT
};
typedef struct
{
int deadzone; // percent of the stick's travel ignored around the centre (0..30)
int range; // N64 stick range at full tilt (the real stick reaches about 80)
int pak; // 0 none, 1 Controller Pak (memory), 2 Rumble Pak
int invert; // the analog stick's (left stick's) axes turned round: bit 0 X, bit 1 Y
int invert_c; // the C buttons' right stick turned round: bit 0 X (C-left/right), bit 1 Y (C-up/down)
int invert_c; // the right stick turned round: bit 0 X (left/right), bit 1 Y (up/down)
unsigned char layout[N64PS5_PAD_COUNT]; // N64PS5_N64_* for each N64PS5_PAD_*
} n64ps5_input_options;
// The layout of the N64 pad by position: Cross A, Square B, L2 / R2 Z, L1 / R1 L / R, OPTIONS Start, D-pad
// D-pad, right stick and Triangle / Circle the C buttons.
void n64ps5_input_default_layout(unsigned char layout[N64PS5_PAD_COUNT]);
void n64ps5_input_set_options(const n64ps5_input_options* opt);
// While the frontend uses the pad itself (a hotkey held, the pause menu just closed), the game sees no
// buttons.
+9 -4
View File
@@ -48,7 +48,7 @@ m64p_system_type g_system = SYSTEM_NTSC;
std::atomic<bool> g_mute{false};
std::atomic<bool> g_fast_forward{false};
std::atomic<int> g_rate{0};
int g_volume = 100;
std::atomic<int> g_volume{100};
// resampler state
double g_pos = 0.0; // position between g_prev and the next input sample
@@ -211,7 +211,7 @@ void AiLenChanged()
g_out.resize(size_t(max_out) * 2);
int16_t* out = g_out.data();
int n = 0;
const int vol = g_volume;
const int vol = g_volume.load(std::memory_order_relaxed);
// Linear interpolation between g_prev (position 0) and each next sample (position 1).
for (int i = 0; i < in_frames; i++)
{
@@ -262,7 +262,7 @@ void VolumeDown()
int VolumeGetLevel()
{
return g_mute.load() ? 0 : g_volume;
return g_mute.load() ? 0 : g_volume.load();
}
void VolumeSetLevel(int level)
@@ -276,7 +276,7 @@ const char* VolumeGetString()
if (g_mute.load())
snprintf(text, sizeof(text), "Mute");
else
snprintf(text, sizeof(text), "%d%%", g_volume);
snprintf(text, sizeof(text), "%d%%", g_volume.load());
return text;
}
@@ -309,6 +309,11 @@ extern "C" void n64ps5_audio_set_mute(bool mute)
g_mute.store(mute);
}
extern "C" void n64ps5_audio_set_volume(int percent)
{
g_volume.store(percent < 0 ? 0 : percent > 100 ? 100 : percent);
}
extern "C" void n64ps5_audio_set_fast_forward(bool on)
{
g_fast_forward.store(on);
+81 -21
View File
@@ -1,13 +1,15 @@
// Mupen64Plus PS5: the input plugin, DualSense / DualShock through libScePad (ProsperoInput.h).
//
// The frontend polls the pads once per video interrupt (fe_emu.cpp); GetKeys turns that state into the
// N64 controller's buttons and stick. Mapping (by position, as on the N64 pad):
// N64 controller's buttons and stick. The left stick is the analog stick; every other control presses the
// N64 button the controller layout gives it (n64ps5_input_options.layout, per game). The default layout, by
// position as on the N64 pad:
//
// left stick -> analog stick D-pad -> D-pad
// Cross -> A Square -> B
// L2 or R2 -> Z L1 / R1 -> L / R
// right stick -> C buttons Triangle -> C-up, Circle -> C-down
// OPTIONS -> Start
// OPTIONS -> Start L3, R3 -> nothing
//
// Rumble: the core sends Rumble Pak writes through ControllerCommand; they drive the controller's motors.
//
@@ -33,7 +35,8 @@ constexpr int kNumControllers = 4;
bool g_init = false;
CONTROL* g_controls = nullptr;
n64ps5_input_options g_opt = {12, 80, 1, 0, 0};
n64ps5_input_options g_opt = {12, 80, 1, 0, 0, {}};
bool g_opt_set = false;
std::atomic<bool> g_block{false};
// One stick axis (0..255, centre 128) -> -range..range, with a dead zone and a linear ramp after it.
@@ -95,6 +98,8 @@ int PakPlugin()
void InitiateControllers(CONTROL_INFO info)
{
if (!g_opt_set) // before the frontend's first options: the default layout
n64ps5_input_default_layout(g_opt.layout);
g_controls = info.Controls;
if (!g_controls)
return;
@@ -122,25 +127,50 @@ void GetKeys(int control, BUTTONS* keys)
return;
const uint32_t b = p.raw_buttons; // the D-pad only (the stick is the analog stick here)
keys->R_DPAD = (b & SCE_PAD_BUTTON_RIGHT) != 0;
keys->L_DPAD = (b & SCE_PAD_BUTTON_LEFT) != 0;
keys->D_DPAD = (b & SCE_PAD_BUTTON_DOWN) != 0;
keys->U_DPAD = (b & SCE_PAD_BUTTON_UP) != 0;
keys->START_BUTTON = (b & SCE_PAD_BUTTON_OPTIONS) != 0;
keys->Z_TRIG = (b & (SCE_PAD_BUTTON_L2 | SCE_PAD_BUTTON_R2)) != 0;
keys->B_BUTTON = (b & SCE_PAD_BUTTON_SQUARE) != 0;
keys->A_BUTTON = (b & SCE_PAD_BUTTON_CROSS) != 0;
keys->L_TRIG = (b & SCE_PAD_BUTTON_L1) != 0;
keys->R_TRIG = (b & SCE_PAD_BUTTON_R1) != 0;
// what each DualSense control is doing; the right stick counts at half tilt. Settings "Invert left stick"
// and "Invert right stick" (GitHub issue #3) turn the sticks' axes round.
bool on[N64PS5_PAD_COUNT] = {};
on[N64PS5_PAD_CROSS] = (b & SCE_PAD_BUTTON_CROSS) != 0;
on[N64PS5_PAD_CIRCLE] = (b & SCE_PAD_BUTTON_CIRCLE) != 0;
on[N64PS5_PAD_SQUARE] = (b & SCE_PAD_BUTTON_SQUARE) != 0;
on[N64PS5_PAD_TRIANGLE] = (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
on[N64PS5_PAD_L1] = (b & SCE_PAD_BUTTON_L1) != 0;
on[N64PS5_PAD_R1] = (b & SCE_PAD_BUTTON_R1) != 0;
on[N64PS5_PAD_L2] = (b & SCE_PAD_BUTTON_L2) != 0;
on[N64PS5_PAD_R2] = (b & SCE_PAD_BUTTON_R2) != 0;
on[N64PS5_PAD_L3] = (b & SCE_PAD_BUTTON_L3) != 0;
on[N64PS5_PAD_R3] = (b & SCE_PAD_BUTTON_R3) != 0;
on[N64PS5_PAD_OPTIONS] = (b & SCE_PAD_BUTTON_OPTIONS) != 0;
on[N64PS5_PAD_UP] = (b & SCE_PAD_BUTTON_UP) != 0;
on[N64PS5_PAD_DOWN] = (b & SCE_PAD_BUTTON_DOWN) != 0;
on[N64PS5_PAD_LEFT] = (b & SCE_PAD_BUTTON_LEFT) != 0;
on[N64PS5_PAD_RIGHT] = (b & SCE_PAD_BUTTON_RIGHT) != 0;
const bool rs_right = p.rx > 192, rs_left = p.rx < 64, rs_down = p.ry > 192, rs_up = p.ry < 64;
const bool rs_flip_x = (g_opt.invert_c & 1) != 0, rs_flip_y = (g_opt.invert_c & 2) != 0;
on[N64PS5_PAD_RS_RIGHT] = rs_flip_x ? rs_left : rs_right;
on[N64PS5_PAD_RS_LEFT] = rs_flip_x ? rs_right : rs_left;
on[N64PS5_PAD_RS_DOWN] = rs_flip_y ? rs_up : rs_down;
on[N64PS5_PAD_RS_UP] = rs_flip_y ? rs_down : rs_up;
// C buttons: the right stick (half tilt), plus Triangle / Circle. Settings "Invert left stick" and
// "Invert right stick" (GitHub issue #3) turn the sticks' axes round; Triangle / Circle stay as they are.
const bool c_right = p.rx > 192, c_left = p.rx < 64, c_down = p.ry > 192, c_up = p.ry < 64;
const bool c_flip_x = (g_opt.invert_c & 1) != 0, c_flip_y = (g_opt.invert_c & 2) != 0;
keys->R_CBUTTON = c_flip_x ? c_left : c_right;
keys->L_CBUTTON = c_flip_x ? c_right : c_left;
keys->D_CBUTTON = (c_flip_y ? c_up : c_down) || (b & SCE_PAD_BUTTON_CIRCLE) != 0;
keys->U_CBUTTON = (c_flip_y ? c_down : c_up) || (b & SCE_PAD_BUTTON_TRIANGLE) != 0;
// and what they press on the N64 pad
bool n64[N64PS5_N64_COUNT] = {};
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
if (on[i] && g_opt.layout[i] < N64PS5_N64_COUNT)
n64[g_opt.layout[i]] = true;
keys->A_BUTTON = n64[N64PS5_N64_A];
keys->B_BUTTON = n64[N64PS5_N64_B];
keys->Z_TRIG = n64[N64PS5_N64_Z];
keys->L_TRIG = n64[N64PS5_N64_L];
keys->R_TRIG = n64[N64PS5_N64_R];
keys->START_BUTTON = n64[N64PS5_N64_START];
keys->U_CBUTTON = n64[N64PS5_N64_C_UP];
keys->D_CBUTTON = n64[N64PS5_N64_C_DOWN];
keys->L_CBUTTON = n64[N64PS5_N64_C_LEFT];
keys->R_CBUTTON = n64[N64PS5_N64_C_RIGHT];
keys->U_DPAD = n64[N64PS5_N64_D_UP];
keys->D_DPAD = n64[N64PS5_N64_D_DOWN];
keys->L_DPAD = n64[N64PS5_N64_D_LEFT];
keys->R_DPAD = n64[N64PS5_N64_D_RIGHT];
const int x = Axis(p.lx, g_opt.deadzone, g_opt.range);
const int y = -Axis(p.ly, g_opt.deadzone, g_opt.range); // the N64's Y grows upwards
@@ -214,7 +244,37 @@ extern "C" const m64ps5_library m64ps5_input_lib = {"ps5-dualsense", kSymbols};
extern "C" void n64ps5_input_set_options(const n64ps5_input_options* opt)
{
if (opt)
{
g_opt = *opt;
g_opt_set = true;
}
}
extern "C" void n64ps5_input_default_layout(unsigned char layout[N64PS5_PAD_COUNT])
{
static const unsigned char kDefault[N64PS5_PAD_COUNT] = {
N64PS5_N64_A, // Cross
N64PS5_N64_C_DOWN, // Circle
N64PS5_N64_B, // Square
N64PS5_N64_C_UP, // Triangle
N64PS5_N64_L, // L1
N64PS5_N64_R, // R1
N64PS5_N64_Z, // L2
N64PS5_N64_Z, // R2
N64PS5_N64_NONE, // L3
N64PS5_N64_NONE, // R3
N64PS5_N64_START, // OPTIONS
N64PS5_N64_D_UP,
N64PS5_N64_D_DOWN,
N64PS5_N64_D_LEFT,
N64PS5_N64_D_RIGHT,
N64PS5_N64_C_UP, // right stick
N64PS5_N64_C_DOWN,
N64PS5_N64_C_LEFT,
N64PS5_N64_C_RIGHT,
};
for (int i = 0; i < N64PS5_PAD_COUNT; i++)
layout[i] = kDefault[i];
}
extern "C" void n64ps5_input_block(bool block)
+13 -6
View File
@@ -5,7 +5,8 @@
# The BC7 encoder is a small one: every 4x4 block in mode 6 (two RGBA endpoints, 4-bit indices), endpoints
# from the block's colour bounding box. Plenty for a smooth background.
#
# make_art.py <out-dir> [font.ttf bold.ttf] (needs Pillow and numpy)
# make_art.py <out-dir> [font.ttf bold.ttf] draws its own (needs Pillow and numpy)
# make_art.py <out-dir> --images <icon> <bg> from pictures (app/art/icon.png, app/art/background.png)
#
# SPDX-License-Identifier: MIT
import os
@@ -13,7 +14,7 @@ import struct
import sys
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps
ACCENT = (255, 196, 64)
BLUE = (64, 140, 255)
@@ -145,11 +146,17 @@ def dds_bc7(width, height, data):
def main():
out = sys.argv[1]
black = sys.argv[2] if len(sys.argv) > 2 else "C:/Windows/Fonts/seguibl.ttf"
bold = sys.argv[3] if len(sys.argv) > 3 else "C:/Windows/Fonts/segoeuib.ttf"
os.makedirs(out, exist_ok=True)
make_icon(black, bold).save(os.path.join(out, "icon0.png"), optimize=True)
bg = make_background(black, bold)
if len(sys.argv) > 4 and sys.argv[2] == "--images":
# pictures made elsewhere (app/art/): fitted to the icon's and the background's sizes
ImageOps.fit(Image.open(sys.argv[3]).convert("RGB"), (512, 512), Image.LANCZOS).save(
os.path.join(out, "icon0.png"), optimize=True)
bg = ImageOps.fit(Image.open(sys.argv[4]).convert("RGB"), (3840, 2160), Image.LANCZOS)
else:
black = sys.argv[2] if len(sys.argv) > 2 else "C:/Windows/Fonts/seguibl.ttf"
bold = sys.argv[3] if len(sys.argv) > 3 else "C:/Windows/Fonts/segoeuib.ttf"
make_icon(black, bold).save(os.path.join(out, "icon0.png"), optimize=True)
bg = make_background(black, bold)
bg.save(os.path.join(out, "background-source.png"), optimize=True)
dds = dds_bc7(bg.width, bg.height, bc7_encode(bg))
for name in ("pic0.dds", "pic1.dds"):
+16 -11
View File
@@ -3,10 +3,11 @@
# 1. builds the app from scratch (build-native.bat Clean): build-native\PPSA99064\ and Mupen64PlusPS5.zip. Always
# clean: 0.6.1 was published from an incremental build that didn't start on the console;
# 2. checks that everything is committed and pushed, that release-notes\<contentVersion>.md exists and that
# the tag isn't taken;
# 3. creates the GitHub release: tag = contentVersion (NN.NNN.NNN, from VERSION in ps5\Makefile), asset
# Mupen64PlusPS5.zip, notes from release-notes\<contentVersion>.md (the app shows them when it offers the
# update);
# the version isn't released yet (under either tag style);
# 3. creates the GitHub release: tag = the version as people write it (0.7.4, VERSION in ps5\Makefile; up to
# 0.7.3 the tag was the contentVersion, 00.007.003), asset Mupen64PlusPS5.zip, notes from
# release-notes\<contentVersion>.md (the app shows them when it offers the update). Every app since 0.6.0
# reads both tag styles as numbers (fe_update.cpp ParseVersion), so they compare correctly;
# 4. reads the release back and compares GitHub's SHA-256 of the asset with the local zip (the app checks
# the download against that digest).
#
@@ -42,23 +43,27 @@ try {
$ahead = git rev-list --count origin/main..HEAD
if ([int]$ahead -gt 0) { throw "push your commits first (git push): $ahead not on GitHub" }
$tag = $version
# "release not found" on stderr is the expected answer: PowerShell 5.1 would make it fatal under 'Stop'
$ErrorActionPreference = 'Continue'
& $gh release view $cv --repo $repo 2>&1 | Out-Null
$exists = $LASTEXITCODE -eq 0
$exists = $false
foreach ($t in @($tag, $cv)) {
& $gh release view $t --repo $repo 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { $exists = $true }
}
$ErrorActionPreference = 'Stop'
if ($exists) { throw "release $cv already exists: raise VERSION in ps5\Makefile" }
if ($exists) { throw "release $version already exists: raise VERSION in ps5\Makefile" }
Write-Host "Publishing Mupen64Plus PS5 $version (tag $cv) to $repo"
& $gh release create $cv $zip --repo $repo --target main --title "Mupen64Plus PS5 $version" --notes-file $notes
Write-Host "Publishing Mupen64Plus PS5 $version (tag $tag) to $repo"
& $gh release create $tag $zip --repo $repo --target main --title "Mupen64Plus PS5 $version" --notes-file $notes
if ($LASTEXITCODE -ne 0) { throw 'gh release create failed' }
$local = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
# parsed here: PowerShell 5.1 strips the quotes a --jq filter would need
$release = (& $gh api "repos/$repo/releases/tags/$cv") -join "`n" | ConvertFrom-Json
$release = (& $gh api "repos/$repo/releases/tags/$tag") -join "`n" | ConvertFrom-Json
$remote = ($release.assets | Where-Object { $_.name -eq $asset }).digest
if ($remote -ne "sha256:$local") { throw "GitHub's digest '$remote' doesn't match the local zip (sha256:$local)" }
Write-Host "Done: https://github.com/$repo/releases/tag/$cv (sha256 $local)"
Write-Host "Done: https://github.com/$repo/releases/tag/$tag (sha256 $local)"
Write-Host 'Consoles on 0.6.3 or newer will offer it the next time Mupen64Plus PS5 starts.'
}
finally { Pop-Location }
+7
View File
@@ -0,0 +1,7 @@
## New
- A new icon and background on the home screen.
- A new look for the pause menu and Settings: one panel with switches, a volume slider and icons. In the pause menu, the touchpad resumes, Square resets the game (it asks first) and Triangle goes back to the game list; Save State, Load State and Quit are at the top and bottom of the list.
- Quick save and load: in a game, press R3 (the right stick) to save in the current state slot, and L3 (the left stick) to load it. The pause menu now opens with the touchpad only.
- Cheats: a Cheats page in the pause menu with the GameShark codes of mupen64plus's cheat list (about 560 game versions: infinite lives, unlock everything and more). Switch each one on or off, or pick one of its choices; the game remembers them.
- Controller layout (Settings): choose what each DualSense button does on the N64 controller. Cross, Circle, Square, Triangle, L1, R1, L2 and R2 (separately), OPTIONS, the D-pad and the right stick can each press any N64 button, or nothing. From the game list it changes every game's layout; from the pause menu, only the game you are playing, which keeps it.
- Settings: Volume (0 to 100%), also from the pause menu while you play.