diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..8fa2fa7865 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# The games gallery's table (dm/tools/docs_gallery.py writes README.md's gallery from it): a new game is a row at its end. README.md itself is not merged this way; after a +# conflict in its gallery, run docs_gallery.py. test_docs fails on a game in the table twice +docs/images/gallery.tsv merge=union diff --git a/AGENTS.md b/AGENTS.md index 09c9908849..289ed9e7f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ | Another Switch title | the NSP script (below), then [docs/agents](docs/agents/README.md) | | Packing, building, prewarm lists, power | [docs/development.md](docs/development.md) | | Checking a change before a pull request | [06-dev-loop.md](docs/agents/06-dev-loop.md) | -- Where a new finding goes, once: a fact about one game (its passes, shader keys, measurements, why its profile needs a key or a cut, its status) in that game's note, in the section it belongs to and marked current or resolved; a general mechanism (a host or tool change that holds for other games) as an entry in its `docs/reference/.md` section (Symptom, Cause, Fix, Found on with the game's note linked), then `python3 dm/tools/docs_symptoms.py` to update the symptom index, and a line under the game note's "Found on this game". Link to a finding instead of repeating it. Adding a game takes its note and its row in the games table. Once Mac gameplay testing passes, add it to README's regular games gallery with a screenshot and IPA size; Mac testing is sufficient for inclusion. Add caption notes only for noteworthy features or limitations (such as a 60 fps upgrade or controller requirement); otherwise show only the size. Keep testing milestones, platform and remaining validation in the game note. This file changes only when a rule does; it states no game count (the games table is the count). `dm/tools/tests/test_docs.py` checks the links and anchors, the notes and their table rows, the symptom index, and the length of this file (1,500 words) and of each guide in `docs/agents` (6,000). +- Where a new finding goes, once: a fact about one game (its passes, shader keys, measurements, why its profile needs a key or a cut, its status) in that game's note, in the section it belongs to and marked current or resolved; a general mechanism (a host or tool change that holds for other games) as an entry in its `docs/reference/.md` section (Symptom, Cause, Fix, Found on with the game's note linked), then `python3 dm/tools/docs_symptoms.py` to update the symptom index, and a line under the game note's "Found on this game". Link to a finding instead of repeating it. Adding a game takes its note and its row in the games table. Once Mac gameplay testing passes, add it to README's regular games gallery with a screenshot and IPA size (`docs/images/gallery.tsv`, then `dm/tools/docs_gallery.py`); Mac testing is sufficient for inclusion. Add caption notes only for noteworthy features or limitations (such as a 60 fps upgrade or controller requirement); otherwise show only the size. Keep testing milestones, platform and remaining validation in the game note. This file changes only when a rule does; it states no game count (the games table is the count). `dm/tools/tests/test_docs.py` checks the links and anchors, the notes and their table rows, the symptom index, and the length of this file (1,500 words) and of each guide in `docs/agents` (6,000). - Each game's constants and shader tables form a game profile in `dm/games//`, where `` is the base game's title ID: `game.tsv` plus the tables (and `patches.py` when the rewriter has patches for its builds). Mario's profile is `010015100B514000`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. There is no default game: set it every time (the tools stop and list the profiles otherwise). Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)). - Prefer general optimizations and fixes to game-specific rules. Performance work (frame pacing, power, caches, load times) counts here as much as correctness. First look for a change that holds for every game: Horizon / NVN / Vulkan semantics, or what the binary, its SDK version, its imports or its files show. Use a game-specific rule (a profile key, a rewriter cut, a build-ID gate, a per-game branch in the host) only when it is absolutely necessary, that is, when no general rule can do it. - Find a bug's root cause and fix it there; do not just work around it. Before changing code, find why it happens: the faulting instruction and its decompilation, the value that went wrong and who wrote it, the Horizon / NVN / Vulkan / Metal semantics the host or the translator gets wrong. Then fix that place. A change that only hides the symptom (a stub, a skipped call, a clamp, a retry, a sleep or a longer timeout, a cache flush, a profile key that turns a feature off) is a workaround: use one only when the cause is known and lies where it cannot be fixed (the game's own code, which a rewriter cut changes deliberately, or a platform limit), or when the user asks for a stopgap. Then say so in the code comment and in the game note or reference entry: the cause, or what is still unknown, marked open. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6be155cf5..3c9c36623a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ Never commit or attach game files, keys, title keys or anything extracted from a - **One change per PR, off `main`.** Rebase on `main` rather than merging it in. - **Commits.** Each commit message says what changed and why, in plain English sentences, as the history does: the symptom, the cause, the fix, and what was measured or checked. - **Where findings go.** A fact about one game goes in its note (`dm/games//README.md`). A general mechanism gets an entry in `docs/reference/.md` (Symptom, Cause, Fix, Found on), followed by `python3 dm/tools/docs_symptoms.py`. A new game takes its profile, its note and its row in the games table ([dm/games/README.md](dm/games/README.md)). +- **The README gallery.** After Mac gameplay testing, add its screenshot to `docs/images/` and its row to [gallery.tsv](docs/images/gallery.tsv), then run `python3 dm/tools/docs_gallery.py`. The table preserves your chosen order; notes hold the IPA size and noteworthy features or limitations. Leave the note empty until the IPA size is measured. Concurrent additions merge in the TSV; regenerate README to resolve a conflict inside its gallery. Conflicting rows for one game need a manual resolution before regeneration. - **The shader chain.** After changing one of its tools, run `python3 dm/gfx/tools/dmsh_chain.py --header` and commit `dm/gfx/nvn/dmsh_chain.h` with the change. Every game's shaders are then rebuilt, so say which games you checked. - **Tests.** CI runs the host tests, the CPU-only graphics tests, the tool tests and the builds ([Continuous integration](docs/development.md#continuous-integration)). GitHub's runners have no Metal 4, so run the GPU tests on an Apple-silicon Mac: `make -C dm/gfx/tests GAME= run`. - **The PR text.** Fill in the template: what and why, what was checked (tests, games, devices), and which docs changed. diff --git a/README.md b/README.md index 8a4a3f8b5a..87bdae2fe6 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Needs iOS / iPadOS 26 or later on an iPhone 15 Pro or newer (A17 Pro chip or lat Under each game: the IPA's size, and "60 fps with 1.2.0" where the game runs at 60 fps (30 on the Switch) with that update. + @@ -166,6 +167,7 @@ Under each game: the IPA's size, and "60 fps with 1.2.0" where the game runs at
Mario Kart 8 Deluxe
Mario Kart 8 Deluxe
~11.9 GB · no Mii heads
FINAL FANTASY CRYSTAL CHRONICLES Remastered Edition
FINAL FANTASY CRYSTAL CHRONICLES Remastered Edition
+ ## Pack an IPA diff --git a/dm/tools/docs_gallery.py b/dm/tools/docs_gallery.py new file mode 100644 index 0000000000..4944caa2f9 --- /dev/null +++ b/dm/tools/docs_gallery.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Writes the games gallery in README.md (between its gallery markers) from docs/images/gallery.tsv: one cell per row of the table, three to a row, in the table's order. +# The table is the source: a new game is a row in it (usually at the end), so two pull requests that each add a game change one line each instead of moving every cell after +# the first one; .gitattributes merges the table with the union driver. Columns: image (a file in docs/images), name (the caption and the image's alt text), note (the small +# line under it: the IPA's size, then " · " and a remark if any; omit when the size is unknown) and, optionally, the base game's title ID, which links to its game note. +# Run it after changing the table; --check only reports whether README.md is current (dm/tools/tests/test_docs.py runs it). +# Usage: docs_gallery.py [--check] +import html, pathlib, re, sys + +REPO = pathlib.Path(__file__).resolve().parents[2] +TABLE = REPO / 'docs' / 'images' / 'gallery.tsv' +OUT = REPO / 'README.md' +BEGIN = '' +END = '' +PER_ROW = 3 + + +def rows(): + out = [] + seen = {'image': {}, 'name': {}, 'title ID': {}} + for n, line in enumerate(TABLE.read_text(encoding='utf-8').splitlines(), 1): + if not line.strip() or line.startswith('#'): continue + c = line.split('\t') + if len(c) not in (2, 3, 4): sys.exit(f'{TABLE.name}:{n}: 2 to 4 columns expected, {len(c)} found') + image, name, note, tid = c + [''] * (4 - len(c)) + if not (REPO / 'docs' / 'images' / image).is_file(): sys.exit(f'{TABLE.name}:{n}: no image docs/images/{image}') + if tid and not re.fullmatch(r'[0-9A-F]{16}', tid): sys.exit(f'{TABLE.name}:{n}: a title ID is 16 hex digits: {tid}') + # A union merge can retain two different captions for the same game, so check its image and title ID too. + for field, value in (('image', image), ('name', name), ('title ID', tid)): + if not value: continue + if value in seen[field]: + sys.exit(f'{TABLE.name}:{n}: duplicate {field} {value!r} (first at line {seen[field][value]}): merge the rows by hand') + seen[field][value] = n + out.append((image, name, note, tid)) + return out + + +def cell(image, name, note, tid): + # Attributes use double quotes; apostrophes in existing captions need no escaping. + image, name, note = (html.escape(s, quote=False).replace('"', '"') for s in (image, name, note)) + caption = f'{name}' if tid else name + detail = f'
{note}' if note else '' # a game whose IPA size is still open has only its caption + return f'{name}
{caption}{detail}' + + +def gallery(): + r = rows() + lines = [''] + for i in range(0, len(r), PER_ROW): + row = [cell(*x) for x in r[i:i + PER_ROW]] + lines += [''] + row + [''] * (PER_ROW - len(row)) + [''] # a short last row is filled with empty cells, as README had it + return '\n'.join(lines + ['
']) + + +def readme(): + text = OUT.read_text(encoding='utf-8') + if text.count(BEGIN) != 1 or text.count(END) != 1: sys.exit(f'README.md: the gallery markers ({BEGIN} … {END}) must appear once each') + head, rest = text.split(BEGIN) + if END not in rest: sys.exit('README.md: the gallery end marker must follow the begin marker') + return head + BEGIN + '\n' + gallery() + '\n' + END + rest.split(END, 1)[1] + + +if __name__ == '__main__': + text = readme() + if '--check' in sys.argv[1:]: + if OUT.read_text(encoding='utf-8') != text: sys.exit('README.md: the gallery is out of date: run dm/tools/docs_gallery.py') + else: + OUT.write_text(text, encoding='utf-8') diff --git a/dm/tools/tests/test_docs.py b/dm/tools/tests/test_docs.py index 4fa5625980..84adf9d483 100644 --- a/dm/tools/tests/test_docs.py +++ b/dm/tools/tests/test_docs.py @@ -1,17 +1,20 @@ """The repository's own Markdown stays navigable: every relative link and #anchor resolves (GitHub's heading anchors), every game profile has its note (dm/games//README.md) and a row in the games table of dm/games/README.md, the symptom index (docs/agents/symptoms.md) is the one dm/tools/docs_symptoms.py writes from docs/reference, and the entry points stay short: AGENTS.md at most 1,500 words, each numbered guide in docs/agents at most 6,000 (a new finding goes into a game note or a -reference, not into these). Third-party trees are left out. No game files needed.""" +reference, not into these), and README's games gallery is the one dm/tools/docs_gallery.py writes from docs/images/gallery.tsv. Third-party trees are left out. No game files needed.""" from pathlib import Path import re import subprocess import sys +import tempfile import unittest +from unittest.mock import patch from urllib.parse import unquote REPO = Path(__file__).resolve().parents[3] sys.path.insert(0, str(REPO / 'dm/tools')) import docs_symptoms # noqa: E402 +import docs_gallery # noqa: E402 SKIP = ('third_party', 'node_modules') AGENTS_WORDS, GUIDE_WORDS = 1500, 6000 @@ -84,6 +87,88 @@ def test_index_is_current(self): self.assertEqual(docs_symptoms.OUT.read_text(), docs_symptoms.index(), 'docs/agents/symptoms.md is out of date: run dm/tools/docs_symptoms.py') +class Gallery(unittest.TestCase): + def test_gallery_is_current(self): + self.assertEqual(docs_gallery.OUT.read_text(encoding='utf-8'), docs_gallery.readme(), 'README.md: the gallery is out of date: run dm/tools/docs_gallery.py') + + def test_plain_text_captions_and_unknown_size(self): + image = docs_gallery.rows()[0][0] + with tempfile.TemporaryDirectory() as folder: + table = Path(folder) / 'gallery.tsv' + table.write_text(f'{image}\tA & B: "Adventure" <2>\n', encoding='utf-8') + with patch.object(docs_gallery, 'TABLE', table): + text = docs_gallery.gallery() + self.assertIn('alt="A & B: "Adventure" <2>"', text) + self.assertIn('
A & B: "Adventure" <2>', text) + self.assertNotIn('', text) + self.assertIn('~2 GB & more', docs_gallery.cell(image, 'Game', '~2 GB & more', '')) + + def test_reversed_markers_fail_before_writing(self): + with tempfile.TemporaryDirectory() as folder: + out = Path(folder) / 'README.md' + text = f'{docs_gallery.END}\nKeep this text\n{docs_gallery.BEGIN}\n' + out.write_text(text, encoding='utf-8') + with patch.object(docs_gallery, 'OUT', out), self.assertRaisesRegex(SystemExit, 'end marker must follow'): + docs_gallery.readme() + self.assertEqual(out.read_text(encoding='utf-8'), text) + + def test_concurrent_additions_merge_and_regenerate(self): + games = docs_gallery.rows()[:5] + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + table, out = root / 'docs/images/gallery.tsv', root / 'README.md' + table.parent.mkdir(parents=True) + (root / '.gitattributes').write_bytes((REPO / '.gitattributes').read_bytes()) + + def git(*args, check=True): + return subprocess.run(['git', '-c', 'user.name=seedds', '-c', 'user.email=seedds@gmail.com', + '-c', 'commit.gpgSign=false', '-c', 'core.hooksPath=/dev/null', *args], + cwd=root, check=check, capture_output=True, text=True) + + def write(rows): + table.write_text(''.join('\t'.join(row).rstrip('\t') + '\n' for row in rows), encoding='utf-8') + out.write_text(f'Before\n{docs_gallery.BEGIN}\n{docs_gallery.END}\nAfter\n', encoding='utf-8') + with patch.object(docs_gallery, 'TABLE', table), patch.object(docs_gallery, 'OUT', out): + out.write_text(docs_gallery.readme(), encoding='utf-8') + git('add', '.') + git('commit', '-m', 'Gallery fixture') + + git('init', '-b', 'base') + write(games[:3]) + git('switch', '-c', 'ours') + write(games[:4]) + git('switch', '-c', 'theirs', 'base') + write(games[:3] + games[4:]) + self.assertEqual(git('merge', '--no-commit', '--no-ff', 'ours', check=False).returncode, 1) + self.assertIn('<<<<<<<', out.read_text(encoding='utf-8')) + self.assertEqual(git('diff', '--name-only', '--diff-filter=U').stdout.strip(), 'README.md') + with patch.object(docs_gallery, 'TABLE', table), patch.object(docs_gallery, 'OUT', out): + self.assertCountEqual(docs_gallery.rows(), games) + text = docs_gallery.readme() + self.assertNotIn('<<<<<<<', text) + self.assertTrue(text.startswith(f'Before\n{docs_gallery.BEGIN}\n')) + self.assertTrue(text.endswith(f'\n{docs_gallery.END}\nAfter\n')) + self.assertEqual(text.count('/reports/`): one By what the change touches (the largest that applies): -- **Docs only:** `python3 -m unittest dm/tools/tests/test_docs.py`; after a change to `docs/reference`, `python3 dm/tools/docs_symptoms.py`. +- **Docs only:** `python3 -m unittest dm/tools/tests/test_docs.py`; after a change to `docs/reference`, `python3 dm/tools/docs_symptoms.py`; after editing `docs/images/gallery.tsv`, `python3 dm/tools/docs_gallery.py` (also resolves conflicts inside README's generated gallery). - **One game's profile or note** (`dm/games//`): the Python tool tests; a full shader build of that game after a change to its shader inputs; the game on the Mac (or the iPhone) along its route, with `scripts/bench.py` numbers when the change is about speed; the finding in the game's note ([AGENTS.md](../../AGENTS.md)). - **The host, the chain or the tools** (`dm/host`, `dm/gfx`, `dm/tools`, `dm/gfx/tools`, `dm/ios`, `scripts`): everything above, `make -C dm/gfx/tests GAME= run`, `dmsh_chain.py --header` after a chain change, `python3 scripts/regress.py impact main..HEAD` to see which games it reaches, then `python3 scripts/regress.py check --games ` (`--quick` while iterating, the full run before the pull request), and its `report.md` pasted into the pull request with a word on every `CHANGED`, `NEW-FAIL` or `REGRESSION`. Choose the games, not every local one: the five the change is most likely to reach or break (those `impact` names, then games that cover the APIs, engines and paths it touches). Take more than five only when the change truly needs them (a broad chain or host change no five games cover), and fewer when its reach is narrow and clear (a fix on one game's path, a tool only some games use). Name the games the report covers and why you chose them: the others are unchecked. diff --git a/docs/images/gallery.tsv b/docs/images/gallery.tsv new file mode 100644 index 0000000000..17371c0852 --- /dev/null +++ b/docs/images/gallery.tsv @@ -0,0 +1,83 @@ +# The games gallery in README.md, in the order it shows them (dm/tools/docs_gallery.py writes it from this table). A new game: its screenshot in docs/images and a row here, +# usually at the end. Columns: image, name (caption and alt text), optional note (IPA size, then " · " and a remark), and optionally the title ID that links the name to its note. +mario-kart-8-deluxe.jpg Mario Kart 8 Deluxe ~11.9 GB · no Mii heads +mario-vs-donkey-kong.jpg Mario vs. Donkey Kong ~1.6 GB +paper-mario-the-thousand-year-door.jpg Paper Mario: The Thousand-Year Door ~5 GB · 60 fps with 1.0.1 +super-mario-3d-world.jpg Super Mario 3D World + Bowser's Fury ~3 GB +super-mario-bros-wonder.jpg Super Mario Bros. Wonder ~4 GB +super-mario-galaxy.jpg Super Mario Galaxy ~3 GB +super-mario-galaxy-2.jpg Super Mario Galaxy 2 ~3 GB +super-mario-odyssey.jpg Super Mario Odyssey ~6 GB +zelda-breath-of-the-wild.jpg The Legend of Zelda: Breath of the Wild ~16 GB · 60 fps with 1.9.0 +zelda-links-awakening.jpg The Legend of Zelda: Link's Awakening ~6 GB +zelda-tears-of-the-kingdom.jpg The Legend of Zelda: Tears of the Kingdom ~17 GB · 60 fps with 1.4.3 +13-sentinels.jpg 13 Sentinels: Aegis Rim ~5 GB +absolum.jpg Absolum ~2 GB +ace-attorney-investigations.jpg Ace Attorney Investigations Collection ~7 GB +advance-wars-1-2-re-boot-camp.jpg Advance Wars 1+2: Re-Boot Camp ~7.1 GB +ai-the-somnium-files.jpg AI: THE SOMNIUM FILES ~8.3 GB +ai-the-somnium-files-nirvana-initiative.jpg AI: THE SOMNIUM FILES - nirvanA Initiative ~11.6 GB +animal-crossing-new-horizons.jpg Animal Crossing: New Horizons ~10.5 GB +animal-well.png ANIMAL WELL ~54 MB +astral-chain.png ASTRAL CHAIN ~9.7 GB · 60 fps target with 1.0.1 01007300020FA000 +balatro.jpg Balatro ~82 MB · played by touch +ball-x-pit.jpg BALL x PIT ~1.1 GB +bayonetta.jpg Bayonetta ~8.8 GB +bayonetta-2.jpg Bayonetta 2 ~12.5 GB +bayonetta-3.jpg Bayonetta 3 ~14.2 GB +big-brain-academy-brain-vs-brain.jpg Big Brain Academy: Brain vs. Brain ~0.8 GB +brain-memory-training-academy.jpg Brain Memory Training Academy ~0.1 GB +captain-toad-treasure-tracker.jpg Captain Toad: Treasure Tracker ~2 GB +catherine-full-body.jpg Catherine: Full Body ~14 GB +celeste.jpg Celeste ~0.9 GB +cuphead.jpg Cuphead ~5 GB +dark-souls-remastered-creation.jpg DARK SOULS: REMASTERED ~4 GB · 60 fps with 1.0.3 +digimon-story-cyber-sleuth.jpg Digimon Story Cyber Sleuth: Complete Edition ~5 GB +donkey-kong-country-tropical-freeze.jpg Donkey Kong Country: Tropical Freeze ~7.3 GB +dragon-quest-vii-reimagined.jpg DRAGON QUEST VII Reimagined ~5.9 GB +dragon-quest-xi-s.jpg DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition ~15 GB · 60 fps with 1.0.4 +final-fantasy-resonance.jpg FINAL FANTASY RESONANCE ~10 GB · 60 fps with 1.0.1 +final-fantasy-tactics.jpg FINAL FANTASY TACTICS - The Ivalice Chronicles ~11 GB +fire-emblem-engage.jpg Fire Emblem Engage ~15 GB · 60 fps with 2.0.0 +fire-emblem-three-houses.jpg Fire Emblem: Three Houses ~13 GB · 60 fps with 1.2.0 +ghost-trick-phantom-detective.jpg Ghost Trick: Phantom Detective ~3 GB +the-great-ace-attorney-chronicles.jpg The Great Ace Attorney Chronicles ~4 GB +hades.jpg Hades ~4 GB +hades-ii.jpg Hades II ~4 GB +hollow-knight-silksong.jpg Hollow Knight: Silksong ~6.4 GB +hyrule-warriors-age-of-calamity.jpg Hyrule Warriors: Age of Calamity ~15 GB · 60 fps with 1.3.0 +kirby-and-the-forgotten-land.jpg Kirby and the Forgotten Land ~6.4 GB +luigis-mansion-3.jpg Luigi's Mansion 3 ~7.5 GB · 60 fps with 1.4.0 +metroid-dread.jpg Metroid Dread ~5 GB +monster-hunter-rise.jpg Monster Hunter Rise ~22 GB · 60 fps with 16.0.2 +monster-train-2.jpg Monster Train 2 ~1.6 GB · 60 fps with 1.1.1 +octopath-traveler.jpg OCTOPATH TRAVELER ~3.6 GB +octopath-traveler-0.jpg Octopath Traveler 0 ~4 GB · 60 fps with 1.0.5 +octopath-traveler-ii.jpg Octopath Traveler II ~5.9 GB · 60 fps with 1.1.1 +patricks-parabox.jpg Patrick's Parabox ~0.2 GB +persona-5-royal.jpg Persona 5 Royal ~14.9 GB +pikmin-4.jpg Pikmin 4 ~10.8 GB +pokemon-brilliant-diamond.jpg Pokémon Brilliant Diamond ~4.6 GB · 60 fps with 1.3.0 +pokemon-legends-z-a.jpg Pokémon Legends: Z-A ~6 GB +pokemon-scarlet.png Pokémon Scarlet ~7.0 GB +pokemon-sword.jpg Pokémon Sword ~12.4 GB +pokemon-shield.jpg Pokémon Shield ~12.4 GB +portal.jpg Portal ~2.6 GB +raging-loop.jpg Raging Loop ~0.9 GB +rhythm-heaven-groove.jpg Rhythm Heaven Groove ~3 GB +romance-of-the-three-kingdoms-8-remake.jpg ROMANCE OF THE THREE KINGDOMS 8 REMAKE ~16.8 GB · controller only +sea-of-stars.jpg Sea of Stars ~5.1 GB +shin-megami-tensei-v-vengeance.jpg SHIN MEGAMI TENSEI V: Vengeance ~14.6 GB · 60 fps with 1.0.3 +stardew-valley.jpg Stardew Valley ~0.6 GB +steins-gate-reboot.jpg STEINS;GATE RE:BOOT ~6 GB +super-smash-bros-ultimate.jpg Super Smash Bros. Ultimate ~17.5 GB · offline +taiko-risshiden-v-dx.jpg Taiko Risshiden V DX ~3.3 GB · Japanese · played by touch +unicorn-overlord.jpg Unicorn Overlord ~4 GB +yoshis-crafted-world.jpg Yoshi's Crafted World ~6.1 GB 01006000040C2000 +nier-automata.png NieR:Automata The End of YoRHa Edition ~10.9 GB · 60 fps with 1.0.3 +zelda-echoes-of-wisdom.jpg The Legend of Zelda: Echoes of Wisdom ~7.8 GB +donkey-kong-country-returns-hd.png Donkey Kong Country Returns HD ~11.6 GB +mario-rabbids-sparks-of-hope.jpg Mario + Rabbids Sparks of Hope ~9.8 GB +xenoblade-chronicles-2.jpg Xenoblade Chronicles 2 ~17.1 GB +xenoblade-chronicles-3.jpg Xenoblade Chronicles 3 ~19.2 GB +final-fantasy-crystal-chronicles.jpg FINAL FANTASY CRYSTAL CHRONICLES Remastered Edition