- C 67%
- Python 13.3%
- JavaScript 7.3%
- HTML 5.6%
- GLSL 2.7%
- Other 4.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
It shows a row of icons and launches one of them. Everything else in here is the part nobody asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| .github/workflows | ||
| assets | ||
| docs | ||
| LICENSES | ||
| romfs | ||
| shared | ||
| source | ||
| tests | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| Makefile | ||
| NOTICE | ||
| PROVENANCE.md | ||
| README.md | ||
Nothing special. Just a homebrew menu for the Nintendo Switch.
* with a UE5-class depth-of-field and motion-blur 3D deferred rendering pipeline
† and a photo mode out of Ghost of Tsushima
A drawing, like every figure here. The numbers in it are the shipped ones; the softness is drawn.
It comes with 4IFIR and updates from the
console, so most people never need this page. On its own it is one file,
hbmenu.nro, into the root of the SD card, over whatever hbmenu is there. Any
setup, whether or not it has ever heard of the rest.
One axis
Apps sit left to right by when you last opened them. That is the whole idea. Open something an hour ago and it is under your thumb; leave something since spring and it is a long way right, small and out of focus.
Six places to be: pinned, today, this week, this month, earlier, never. No folders, no tags, no categories, no search box in the strip. One axis, and nothing gets to build a second one on top of it.
The gaps in time are wildly uneven. An hour, a week, half a year. The strip flattens them, and every tile takes the same width of screen whatever the real gap. The order tells the truth; the spacing is a courtesy. Pinned apps come off the axis altogether and gather at the left.
A launches, straight into the handoff. X pins. Y deletes, and for four seconds X takes it back. − filters, + opens the menu, B is HOME immediately — there is no exit animation, deliberately.
The filter brings its own keyboard: the system one crashes the console when a homebrew menu dismisses it.
Which of the six a tile lands in is worked out fresh each time the list is read, out of one date. Nothing on your card ever says "this one is a Today app".
That date is the menu's own, written the moment you launch. A memory card keeps its own record of when a file was last touched, and it cannot be trusted. An axis built on that would reshuffle itself for reasons you could never see.
Set the console's clock back a day and, without one careful line, every app would leap to the far right at once, as though nothing had been opened in a few hundred billion years. That line lands them in Today instead. Still wrong, and wrong in a way you can read.
Each app keeps fourteen days of its own history, four bytes for the whole thing. Those are the dots under the tile and the sparkline in the panel above. Nothing is written to your card while you move around — only when you launch, when you pin, and on the way out. Cards wear out.
When it does write, the new file goes down beside the old one and is only swapped in once it is whole. Pull the power out halfway through and the library is still there.
Open the menu and the tiles are dealt in. Which of the three openings you get is decided at boot, and it is over inside a second.
Why a menu has a lens
Nothing about a menu needs a lens. This one has one anyway.
The row of tiles is not a picture. It is a room with tiles standing in it, and what you see is a camera pointed at that room. The tile under the cursor stands on the focal plane. The others step back as they get older, and the lens softens each one by how far back it stands. What is far away looks far away.
The ladder is flat near the cursor and steep past it. The nearest tiles stay sharp enough to read, and everything from five out sits at one far depth, softened by the same amount, so the end of the library reads as texture rather than as a list.
The lens is never told what that curve was. It asks one thing of each pixel: how far is this from the place that is in focus. That distance is the whole instruction.
The sharp place does not travel with the selection. The cursor is fixed on the screen, and the library walks through a blur field that stands still.
To soften one pixel the shader looks at thirty-six places around it. They sit in three rings, turned against one another by the golden angle — the angle that keeps sunflower seeds from lining up. What is behind is gathered apart from what is in front, so a sharp tile never bleeds into the soft one behind it. Where the blur is small, or the frame is moving fast, twenty places are enough. On a disc that narrow, thirty-six is more looking than the answer needs.
The loop that reads those thirty-six places has to be written so the compiler can count it in advance. Written the other way, the chip moved the table of places out of its fast registers and into memory, and the menu ran at one frame a second. Nothing errored and nothing warned. There is a comment above that loop now, addressed to whoever touches it next.
Every fragment writes its own depth: surfaces, glyph edges, the antialiasing on a rounded corner. An earlier build let the small things inherit the depth of the card they sat on, and a letter hanging over the edge of a soft tile stayed razor-sharp against the background. Tiles that were supposed to be out of focus kept their edges, and nothing done later in the blur could take them away.
That is why the renderer is built the way it is. Softness of this kind cannot be painted over the strip afterwards; each pixel has to arrive carrying how far away it stood. Bands cut across the screen, rectangles scissored around the sharp part, a list of which tiles to blur handed up from the processor — all tried, all named and retired.
The lens before this one was built for a blur it never had to make. It ground each frame down through four sizes so every ring of the pattern could pick a matching one, which is what you do to open a disc thirty or a hundred pixels wide. At 720p this one never opens one past fourteen, and docked it scales up with the picture. The old one was paying, every frame, for machinery it could not use, and a good part of what it cost went on starting and stopping passes before a single useful sample.
Before a change to the blur goes near a console, a script on a desktop works out what a real round aperture would have done to the same picture, and the two are put side by side. It asks four things. Is what sits on the focal plane still untouched. Does a near tile melt over the page the way glass does. Does a sharp edge survive the focus racking past it. Does the far field carry the right amount of light.
Only the first of those sweeps an area — everything standing on the focal plane. The other three read a single line across the picture, and whatever went wrong off that line is never seen.
That check runs before anything is merged. It checks the maths and not the shader that ships, so a change made only in the shader sails straight through. Catching that one still takes a console.
There is a light theme, and it pulls the blur in. A disc that reads as depth on black reads as a printing fault on white.
Depth is how the axis becomes visible.
Photo mode
Because it is a scene and not a picture, you can walk around inside it. Photo mode hands you the camera. The right stick orbits the library while it is still running; the triggers reach for a wider or a longer lens. A rail down the side holds focus, aperture and zoom, then six ways to grade the picture, and six film looks on the end — the first of them being none at all. The aperture opens further here than the menu ever asks for on its own. Then it writes the picture to the card with its own controls taken back out of the frame.
The frame
Every stage checks that it has what it needs. One that is missing something is skipped and the frame carries on. An effect can go missing; the frame does not. Under all of it is a flat path with no depth at all, and under that a plain error page. Neither is a black screen.
One frame, read five ways. Colour is what reaches the screen. Velocity is one arrow a tile, pointing back to where that tile was a frame ago. It reads zero everywhere until something moves, which is why the drawing steps the strip along: standing still, that band is empty. Depth is one flat value a tile. The fourth band is the disc each pixel earns for standing where it does — the circle of confusion, drawn at the size it really is. The last band is whatever was bright enough to bloom. On the light theme that would be the page rather than the type, which is exactly why the glow fades out to nothing as the theme brightens. Under all five runs the coarse grid that the lens and the blur both read.
Some of what went into that graph was built, looked at, and taken back out.
A depth buffer you mean to look at again has to be asked for the slow way. Ask for the fast one and nothing errors, nothing warns, and the blur quietly leaves the whole screen.
Compute shaders were tried. On this chip, telling one of them to wait for the one before it means stopping the entire graphics pipe and waiting for it to go quiet, and a frame wanted six of those in a row. No cleverness gets around it. There are fifteen shaders here and not one of them is compute. The hardware decided that, not the author. A modern upscaler had been built on top of them and went out with them; its slot is still in the graph, empty.
Softening the whole frame while the strip moved lasted one build. The note left behind in the code says why: things standing perfectly still going soft reads as upscaler mud, worse than nothing.
Not every removal survived. Rotating the blur pattern for every pixel is the textbook thing to do, and there was a good argument against it here. That argument won on paper and lost on the panel.
Overclock the console and the menu starts building 1080p frames on its own. It asks 4IFIR what the GPU clock is doing and follows that, not the dock. It changes its mind slowly on purpose, because rebuilding for a new size shows as a hitch — quick to notice you have gone up, reluctant to accept you have come back down. A console already overclocked when the menu starts gets the right size first time and never rebuilds.
Leave it alone for a minute and it settles to thirty frames a second. Nothing is moving. Touch anything and it comes straight back.
Motion
The first attempt smeared every pixel along its own direction. Orbit the camera and the whole picture came apart. It read as damage, not as movement, and it was taken straight back out.
What ships smears things that are actually moving, and nothing else. The shutter is exactly one frame long, the way a camera's is, and anything travelling less than half a pixel is left alone.
That half-pixel rule is also a guard. Ask the arithmetic how fast a still pixel is moving against its still neighbour, and it divides nothing by nothing and hands back a value that is not a number. A pixel standing perfectly still can poison every pixel around it.
Further in
ARCHITECTURE.md is the renderer written down as it is, with
line numbers. docs/tegra-x1.md is the hardware half: what the
console refused to do, and what got built instead.
CONTRIBUTING.md is mostly the ways the build will lie to you.
The plans they came from are kept, dated, in docs/history/, and the design
package they answer to is in docs/spec/. None of them are needed to use the
menu.
A longer form of all of this, with the menu itself running in the browser, is at git.swpulse.ru/pages/cooler3d/latitude-hbmenu.
Not finished
This is a preview. Here is what is not done.
- The catalog is drawn and cannot be opened. The tile that leads in is still on screen, and the only code that ever puts the cursor on it runs on the way back out, so the way in is to have been in already. Arrowing left was stopped at the first app in July, while the part that fetches the catalog was down, and it was never let past again. Installing the sysmodule does not open it: the lock is in the menu's own cursor.
- The catalog needs a list of what is in it, and this repository does not have
one. The menu reads the copy
ldn-catalogleaves on the card, and failing that a copy inside the .nro — which is not checked in, so a build from a fresh clone has nothing to show. Installing and deleting needldn-catalogitself, the piece that lives in 4IFIR and is not here either. Without it Y stops at a gate that names what is missing, rather than half-doing the job. - It does not always hold sixty frames a second. At rest it does; with the strip moving and the blur on, it can go past the frame. The frame was timed on a console once, and the stopwatch turned out to be dividing by the wrong number, so every figure written down that day reads low. Nobody has timed it since. The build that ships also still links the debug graphics library, which checks every call to the GPU and charges frame time for the favour.
- The icon atlas holds sixty-four homebrew. The sixty-fifth gets a letter instead of an icon.
- A downloaded file's checksum is read and never compared. Cancelling an install, and the limit on how many run at once, are in the code with nothing to press.
- Part of the scene is built and not switched on: a lighting pass that runs only behind a debug flag, the upscaler slot left in the graph and empty, and a buffer that was specified and never allocated.
- Settings that stopped meaning anything are still read, noted in the log and ignored, so an old configuration file cannot break the menu. One of those notes almost never gets written: the dead setting has to be spelled out by hand, and every other route leaves it at zero, which is nothing to report.
- Nothing in the menu ever shows you the licence notice it carries.
- NetLoader is gone rather than broken; Y is Delete now. A stub of it outlived the decision by five builds before it went.
- No folder navigation and no grid toggle. Both would build a second axis.
- Two things the console said back that have not been explained. In one capture of the coarse grid, a small part of it matches neither of the two answers the shader is supposed to give. In another, every blurred pixel leans the same way, where by the depth ladder some of them should lean the other. One frame, one number, open. The raw captures are not kept here, so neither can be looked at again from this repository.
- The design package in
docs/spec/was written in Russian and English mixed together, and it was translated and edited to get here. Its own README lists what changed. The code has moved past it in places, and where they disagree the code is what runs.
Build
C, libnx and deko3d. Needs devkitPro with devkitA64 and uam.
make # -> hbmenu.nro, one file, about 2 MB
make DEKO3D=rel # release: links -ldeko3d instead of the validating -ldeko3dd
make DEBUG_LOG=1 # lab only: SD breadcrumb log sdmc:/switch/.hbmenu_debug.log
cd tests && make && ./test_all # host tests, no console in the room
The parts that can be checked without a console are checked without one first — the order of the strip, the six places, the fourteen days, the file on the card.
Licence
Unlicense. Public domain, no conditions, no attribution asked for. Some licences ask for a credit line. Some ask for the same freedom back. This one asks for nothing, which is the reason it was picked.
About 48 lines are not ours to give away. The routine that hands control to the program you launch came from switchbrew/nx-hbmenu, which this is a fork of. That, the fonts and five vendored pieces keep their own terms, listed in NOTICE and counted file by file in PROVENANCE.md. The notices that have to travel with the binary do.
The type is Geist and Geist Mono. The files sitting in romfs/fonts/ are the ones
the menu draws from.