Rewrite the about page for clarity and corrected factual information

This commit is contained in:
Alula 2025-11-24 02:04:31 +01:00
parent 74b699cc3c
commit 7205c16735
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
3 changed files with 503 additions and 45 deletions

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 120
}

View File

@ -0,0 +1,374 @@
const COLORS = {
red: { box: "fill-red-900/50 stroke-red-600", text: "fill-red-300" },
blue: { box: "fill-blue-900/50 stroke-blue-700", text: "fill-blue-300" },
green: { box: "fill-green-900/50 stroke-green-700", text: "fill-green-300" },
gray: { box: "fill-slate-800/50 stroke-slate-600", text: "fill-slate-300" },
cyan: { box: "fill-cyan-900/50 stroke-cyan-700", text: "fill-cyan-300" },
purple: {
box: "fill-purple-900/50 stroke-purple-700",
text: "fill-purple-300",
},
};
// Memory layout configuration - percentages for SVG
const MEMORY_LAYOUT = {
chartHeight: 56,
barHeight: 40,
rowSpacing: 16,
// Width percentages
ipc: 0.35,
kernel: 0.1,
conflict: 0.05,
reserved: 0.2,
user: 0.25,
segments: [
{
id: "ipc",
label: "IPC Message buffer",
range: "0x0",
boxClass: COLORS.cyan.box,
textClass: COLORS.cyan.text,
},
{
id: "kernel",
label: "Kernel",
range: "0x100",
boxClass: COLORS.blue.box,
textClass: COLORS.blue.text,
tooltip: "Various fields written by the kernel",
},
{
id: "conflict",
label: "New!",
range: "0x108-0x110",
boxClass: COLORS.purple.box,
textClass: COLORS.purple.text,
tooltip: "0x108-0x110: Added in 21.0.0 (CPU Time)",
},
{
id: "reserved",
label: "Reserved",
range: "0x110-0x180",
boxClass: COLORS.gray.box,
textClass: COLORS.gray.text,
},
{
id: "user",
label: "User TLS",
range: "0x180-0x200",
boxClass: COLORS.green.box,
textClass: COLORS.green.text,
},
],
} as const;
// Generate background path (fill only, no stroke)
const backgroundPath = (
x: number,
y: number,
width: number,
height: number,
radius: number,
topLeft: boolean,
topRight: boolean,
bottomRight: boolean,
bottomLeft: boolean,
): string => {
const tl = topLeft ? radius : 0;
const tr = topRight ? radius : 0;
const br = bottomRight ? radius : 0;
const bl = bottomLeft ? radius : 0;
return `M ${x + tl} ${y}
L ${x + width - tr} ${y}
${tr > 0 ? `Q ${x + width} ${y} ${x + width} ${y + tr}` : `L ${x + width} ${y}`}
L ${x + width} ${y + height - br}
${br > 0 ? `Q ${x + width} ${y + height} ${x + width - br} ${y + height}` : `L ${x + width} ${y + height}`}
L ${x + bl} ${y + height}
${bl > 0 ? `Q ${x} ${y + height} ${x} ${y + height - bl}` : `L ${x} ${y + height}`}
L ${x} ${y + tl}
${tl > 0 ? `Q ${x} ${y} ${x + tl} ${y}` : `L ${x} ${y}`}
Z`;
};
// Generate stroke path (no inner strokes between segments)
// Case 1: leftmost segment - start at top right, go left, rounded corner, go down, rounded corner, go right
// Case 2: inner segment - two horizontal lines (top and bottom)
// Case 3: rightmost segment - start at top left, go right, rounded corner, go down, rounded corner, go left
const strokePath = (
x: number,
y: number,
width: number,
height: number,
radius: number,
isFirst: boolean,
isLast: boolean,
): string => {
if (isFirst) {
// Leftmost segment: start at top right, go left, rounded corner, go down, rounded corner, go right
return `M ${x + width} ${y}
L ${x + radius} ${y}
Q ${x} ${y} ${x} ${y + radius}
L ${x} ${y + height - radius}
Q ${x} ${y + height} ${x + radius} ${y + height}
L ${x + width} ${y + height}`;
} else if (isLast) {
// Rightmost segment: start at top left, go right, rounded corner, go down, rounded corner, go left
return `M ${x} ${y}
L ${x + width - radius} ${y}
Q ${x + width} ${y} ${x + width} ${y + radius}
L ${x + width} ${y + height - radius}
Q ${x + width} ${y + height} ${x + width - radius} ${y + height}
L ${x} ${y + height}`;
} else {
// Inner segment: two horizontal lines (top and bottom)
return `M ${x} ${y}
L ${x + width} ${y}
M ${x} ${y + height}
L ${x + width} ${y + height}`;
}
};
export function MemoryLayout() {
const { chartHeight, barHeight, rowSpacing, segments } = MEMORY_LAYOUT;
const { ipc, kernel, conflict, reserved } = MEMORY_LAYOUT;
// Calculate absolute positions for 5 segments
const x0 = 0;
const x1 = ipc; // 0x100
const x2 = ipc + kernel; // 0x110
const x3 = ipc + kernel + conflict; // 0x110 (conflict overlaps)
const x4 = ipc + kernel + conflict + reserved; // 0x180
const x5 = 1; // 0x200
// SVG viewBox dimensions (will scale to container width)
const svgWidth = 800;
// Remove labelHeight from calculation since we're integrating labels into bars
const svgHeight = chartHeight + rowSpacing + barHeight + rowSpacing + barHeight;
// Padding to prevent stroke clipping at edges
const padding = 1;
const viewBoxWidth = svgWidth + padding * 2;
const viewBoxHeight = svgHeight + padding * 2;
// Helper to convert percentage to pixel (with padding offset)
const px = (percent: number) => percent * svgWidth + padding;
// Calculate segment width based on index (supports 5 segments)
const getSegmentWidth = (index: number): number => {
if (index === 0) return px(x1) - px(x0);
if (index === 1) return px(x2) - px(x1);
if (index === 2) return px(x3) - px(x2);
if (index === 3) return px(x4) - px(x3);
return px(x5) - px(x4);
};
// Calculate segment start position based on index (supports 5 segments)
const getSegmentStart = (index: number): number => {
if (index === 0) return px(x0);
if (index === 1) return px(x1);
if (index === 2) return px(x2);
if (index === 3) return px(x3);
return px(x4);
};
return (
<div className="space-y-6 not-prose font-sans text-xs mt-6 select-none">
{/* Header */}
<div className="space-y-2">
<div className="text-switch-text-info font-medium flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-switch-text-info"></span>
<span>ThreadLocalRegion Memory Layout (non-linear, for visual reference only)</span>
</div>
{/* SVG Chart - Seamless, maintains aspect ratio */}
<div className="w-full bg-switch-surface-1" style={{ aspectRatio: viewBoxWidth / viewBoxHeight }}>
<svg
viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
className="w-full h-full block"
preserveAspectRatio="xMidYMid meet"
style={{ display: "block" }}
>
{/* Main Memory Layout Chart */}
<g id="main-chart">
{/* Background segments */}
{segments.map((segment, index) => {
const xStart = getSegmentStart(index);
const width = getSegmentWidth(index);
const isFirst = index === 0;
const isLast = index === segments.length - 1;
const borderRadius = 4;
// Extract fill and stroke classes
const classes = segment.boxClass.split(" ");
const fillClass = classes.find((c) => c.startsWith("fill-")) || "";
const strokeClass = classes.find((c) => c.startsWith("stroke-")) || "";
return (
<g key={segment.id}>
{/* Background (fill only) */}
{isFirst || isLast ? (
<path
d={backgroundPath(
xStart,
padding,
width,
chartHeight,
borderRadius,
isFirst, // topLeft
isLast, // topRight
isLast, // bottomRight
isFirst, // bottomLeft
)}
className={fillClass}
fillRule="evenodd"
/>
) : (
<rect x={xStart} y={padding} width={width} height={chartHeight} className={fillClass} />
)}
{/* Stroke (no inner strokes) */}
<path
d={strokePath(xStart, padding, width, chartHeight, borderRadius, isFirst, isLast)}
className={strokeClass}
fill="none"
strokeWidth={1}
/>
{segment.id === "conflict" ? (
<text
x={xStart + width / 2}
y={padding + chartHeight / 2}
textAnchor="middle"
dominantBaseline="middle"
className={segment.textClass}
fontSize="9"
fontWeight="bold"
transform={`rotate(-90 ${xStart + width / 2} ${padding + chartHeight / 2})`}
>
{segment.label}
</text>
) : (
<>
<text
x={xStart + width / 2}
y={padding + chartHeight / 2 - 6}
textAnchor="middle"
dominantBaseline="middle"
className={segment.textClass}
fontSize="11"
fontWeight="bold"
>
{segment.label}
</text>
<text
x={xStart + width / 2}
y={padding + chartHeight / 2 + 6}
textAnchor="middle"
dominantBaseline="middle"
className={segment.textClass}
fontSize="9"
opacity={0.6}
>
{segment.range}
</text>
</>
)}
</g>
);
})}
</g>
{/* Vertical Guide Lines */}
<g id="guide-lines" strokeDasharray="4,4" opacity={0.4}>
<line
x1={px(x1)}
y1={padding + chartHeight}
x2={px(x1)}
y2={padding + svgHeight}
stroke="currentColor"
className="text-switch-border"
strokeWidth={1}
/>
<line
x1={px(x2)}
y1={padding + chartHeight}
x2={px(x2)}
y2={padding + svgHeight}
stroke="currentColor"
className="text-switch-border"
strokeWidth={1}
/>
<line
x1={px(x3)}
y1={padding + chartHeight}
x2={px(x3)}
y2={padding + svgHeight}
stroke="rgb(239 68 68)"
strokeWidth={1}
opacity={0.3}
/>
<line
x1={px(x4)}
y1={padding + chartHeight}
x2={px(x4)}
y2={padding + svgHeight}
stroke="currentColor"
className="text-switch-border"
strokeWidth={1}
/>
</g>
<g id="comparison-rows">
<g id="old-libnx">
<rect
x={px(x2)}
y={padding + chartHeight + rowSpacing}
width={px(x5) - px(x2)}
height={barHeight}
className={COLORS.red.box}
strokeWidth={1}
rx={4}
/>
<text
x={px(x2 + (x5 - x2) / 2)}
y={padding + chartHeight + rowSpacing + barHeight / 2}
textAnchor="middle"
dominantBaseline="middle"
className={COLORS.red.text}
fontSize="10"
fontWeight="600"
>
libnx before v4.10.0
</text>
</g>
<g id="new-libnx">
<rect
x={px(x4)}
y={padding + chartHeight + rowSpacing + barHeight + rowSpacing}
width={px(x5) - px(x4)}
height={barHeight}
className={COLORS.green.box}
strokeWidth={1}
rx={4}
/>
<text
x={px(x4 + (x5 - x4) / 2)}
y={padding + chartHeight + rowSpacing + barHeight + rowSpacing + barHeight / 2}
textAnchor="middle"
dominantBaseline="middle"
className={COLORS.green.text}
fontSize="10"
fontWeight="600"
>
New libnx / Patched / Official software
</text>
</g>
</g>
</svg>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,11 @@
import { Card, CardContent } from "../components/ui/Card"; import { Card, CardContent } from "../components/ui/Card";
import { MemoryLayout } from "../components/MemoryLayout";
const GITHUB_URL = "https://github.com/alula/hbpatcher"; const GITHUB_URL = "https://github.com/alula/hbpatcher";
const DIRTY_HACK_DISCORD_SOURCE =
"https://discord.com/channels/269333940928512010/414949821003202562/1440485257550889221";
const FW_19_FIX_SOURCE =
"https://github.com/Atmosphere-NX/Atmosphere/blob/7aa0bed869c7ed642d5503c6c80e3dc337bc56bd/stratosphere/loader/source/ldr_capabilities.cpp#L428";
export function AboutPage() { export function AboutPage() {
return ( return (
@ -37,6 +42,39 @@ export function AboutPage() {
</ol> </ol>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardContent className="space-y-3">
<h3 className="text-base font-normal">
Found Incompatible Homebrew?
</h3>
<p className="text-switch-text-info text-sm leading-relaxed">
If you encounter unmaintained and closed-source homebrew that
doesn't work with this patcher, please{" "}
<a
href={GITHUB_URL + "/issues"}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
open an issue on GitHub
</a>{" "}
or{" "}
<a
href="https://gbatemp.net/members/alula.601460/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
DM me on GBATemp
</a>
.
</p>
<p className="text-switch-text-info text-sm leading-relaxed">
<strong>Note:</strong> I do not offer support for patching
piracy-related homebrew (such as DBI).
</p>
</CardContent>
</Card>
<Card> <Card>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<h3 className="text-base font-normal">For Developers</h3> <h3 className="text-base font-normal">For Developers</h3>
@ -44,7 +82,7 @@ export function AboutPage() {
Do not rely on this tool. Please recompile your homebrew with{" "} Do not rely on this tool. Please recompile your homebrew with{" "}
<code>libnx v4.10.0</code> (or newer). <code>libnx v4.10.0</code> (or newer).
</div> </div>
<div className="text-switch-text-info text-base leading-relaxed"> <div className="text-switch-text-info text-sm leading-relaxed">
<p> <p>
The updated library resolves the memory conflict and stays The updated library resolves the memory conflict and stays
backward compatible with older Atmosphère versions and backward compatible with older Atmosphère versions and
@ -60,13 +98,31 @@ export function AboutPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardContent className="space-y-3">
<h3 className="text-base font-normal">License</h3>
<p className="text-switch-text-info text-sm">
This project is licensed under the GNU General Public License v2.0
or later (GPL-2.0-or-later). See the{" "}
<a
href={GITHUB_URL + "/blob/master/LICENSE"}
target="_blank"
rel="noopener noreferrer"
>
LICENSE
</a>{" "}
file for details.
</p>
</CardContent>
</Card>
<div className="space-y-8"> <div className="space-y-8">
<section className="space-y-3"> <section className="space-y-3">
<h2 className="text-lg font-bold text-switch-text">TL;DR</h2> <h2 className="text-lg font-bold text-switch-text">TL;DR</h2>
<p className="text-switch-text-info text-sm leading-relaxed"> <p className="text-switch-text-info text-sm leading-relaxed">
Atmosphère 1.10.0+ integrates kernel optimizations from Firmware Atmosphère integrates kernel optimizations from Firmware 21.0.0.
21.0.0. This improves performance but breaks older homebrew, This improves performance but breaks older homebrew compiled with
causing crashes or hangs on exit. If your homebrew is libnx versions before v4.10.0, causing crashes that can occur on
exit, during thread creation, or at runtime. If your homebrew is
unmaintained, use this tool to patch the <code>.nro</code> files. unmaintained, use this tool to patch the <code>.nro</code> files.
If the app is still active, ask the developer to update it. If the app is still active, ask the developer to update it.
</p> </p>
@ -77,21 +133,59 @@ export function AboutPage() {
How this tool works How this tool works
</h3> </h3>
<p className="text-switch-text-info text-sm leading-relaxed"> <p className="text-switch-text-info text-sm leading-relaxed">
This tool scans your <code>.nro</code> files to see if they rely This tool scans your <code>.nro</code> files to see if they
on the old, now-unsafe memory layout. If it detects a conflict, it contain old libnx code that writes to the conflicting memory
modifies the binary to{" "} region. If it detects a conflict, it patches certain{" "}
<a <a
href="https://github.com/switchbrew/libnx/commit/cad06c006e4e0caf9c63755ac6c2a10a52333e27" href="https://github.com/switchbrew/libnx/commit/cad06c006e4e0caf9c63755ac6c2a10a52333e27"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
move its Thread Local Storage (TLS) libnx functions to move their TLS writes
</a>{" "} </a>{" "}
from offset <code>0x108</code> to the safe offset{" "} from the conflicting offset <code>0x108</code> to the safe offset{" "}
<code>0x180</code>. Effectively, it mimics the result of <code>0x180</code>.
recompiling the app with the latest libraries, restoring stability
without needing the source code.
</p> </p>
<p className="text-switch-text-info text-sm leading-relaxed">
Note that the patches applied by this tool do <strong>not</strong>{" "}
move the <code>ThreadVars</code> structure, which resides at the
very end of the region. Effectively, this reduces (squeezes) the
available space for TLS slots. While this works for the vast
majority of homebrew, applications that use an unusually large
number of TLS slots might overwrite critical thread data (and
cause crashes - this is an edge case, patching this is out of
scope for this tool).
</p>
</section>
<section className="space-y-3">
<h3 className="text-base font-bold text-switch-text">
Why the breakage happened
</h3>
<div className="text-switch-text-info text-sm leading-relaxed space-y-3">
<p>
The Thread Local Region is <code>0x200</code> bytes large. The
first <code>0x180</code> bytes are effectively reserved for the
kernel, while the last <code>0x80</code> bytes (
<code>0x180-0x200</code>) are for userland. Official software
respects this split, placing its TLS data at offset{" "}
<code>0x180</code>.
</p>
<p>
Previous versions of <code>libnx</code> chose to start user TLS
slots at <code>0x108</code> (encroaching on the kernel's area)
to maximize the number of available slots. This was safe until
Firmware 21.0.0, where Nintendo added{" "}
<code>cache_maintenance_flag</code> at <code>0x104</code> and{" "}
<code>thread_cpu_time</code> at <code>0x108</code>.
</p>
<p>
Now, the kernel writes to <code>0x108</code> on every thread
switch, overwriting the TLS data that old homebrew stored there.
This corruption causes the crashes.
</p>
<MemoryLayout />
</div>
</section> </section>
<section className="space-y-3"> <section className="space-y-3">
@ -102,18 +196,18 @@ export function AboutPage() {
<p> <p>
It might seem logical for the Homebrew Loader (<code>hbl</code>) It might seem logical for the Homebrew Loader (<code>hbl</code>)
to handle this automatically, but that's out of scope for a to handle this automatically, but that's out of scope for a
simple program loader. HBLs job is to execute code as-is, not simple program loader. HBL's job is to execute code as-is, not
to dynamically repair binaries. to dynamically repair binaries.
</p> </p>
<p> <p>
Trying to patch memory offsets on the fly is invasive and Trying to patch memory offsets on the fly is invasive and
fragile; if HBL guesses wrong, it could corrupt the entire fragile; if HBL guesses wrong, it could break otherwise
homebrew environment. Furthermore, baking a patcher into the functioning homebrew. Furthermore, baking a patcher into the
loader sets a bad precedent. The maintainers want to encourage loader sets a bad precedent. The maintainers want to encourage
proper fixes (recompilation) rather than relying on permanent proper fixes (recompilation) rather than relying on permanent
"dirty hacks" within the OS itself.{" "} "dirty hacks" within the OS itself.{" "}
<a <a
href="https://discord.com/channels/269333940928512010/414949821003202562/1440485257550889221" href={DIRTY_HACK_DISCORD_SOURCE}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -123,28 +217,6 @@ export function AboutPage() {
</div> </div>
</section> </section>
<section className="space-y-3">
<h3 className="text-base font-bold text-switch-text">
Why the breakage happened
</h3>
<div className="text-switch-text-info text-sm leading-relaxed space-y-3">
<p>
This isn't a bug; it's a conflict over real estate. In Firmware
21.0.0, Nintendo optimized the kernel to write CPU timing data
to a specific memory address (<code>usertls + 0x108</code>) on
every thread switch.
</p>
<p>
Previous versions of <code>libnx</code> (the homebrew library)
assumed this space was unused and stored data there. Official
software always respected the reservation, so it wasn't
affected. Now that the kerneland by extension,
Atmosphèreactively uses that space, old homebrew gets its
memory overwritten, leading to immediate crashes.
</p>
</div>
</section>
<section className="space-y-3"> <section className="space-y-3">
<h3 className="text-base font-bold text-switch-text"> <h3 className="text-base font-bold text-switch-text">
Why Atmosphère didn't add a workaround Why Atmosphère didn't add a workaround
@ -157,13 +229,20 @@ export function AboutPage() {
</p> </p>
<p> <p>
While the developers have added quiet workarounds in the past While the developers have added quiet workarounds in the past
(like the FW 19.0.0 debug flags), this specific issue was too (like the{" "}
risky to mask. A workaround here would require adding <a
conditional logic to the kernel schedulerthe most time-critical href={FW_19_FIX_SOURCE}
part of the system. Adding checks for "broken homebrew" during target="_blank"
every thread switch would add complexity, potentially degrade rel="noopener noreferrer"
performance system-wide, and could even introduce bugs in >
official games. FW 19.0.0 debug flags
</a>
), this specific issue was too risky to mask. A workaround here
would require adding conditional logic to the kernel
schedulerthe most time-critical part of the system. Adding
checks for "broken homebrew" during every thread switch would
add complexity, potentially degrade performance system-wide, and
could even introduce bugs in official games.
</p> </p>
</div> </div>
</section> </section>