diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..054d599 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 120 +} diff --git a/src/components/MemoryLayout.tsx b/src/components/MemoryLayout.tsx new file mode 100644 index 0000000..77de2a9 --- /dev/null +++ b/src/components/MemoryLayout.tsx @@ -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 ( +
+ {/* Header */} +
+
+ + ThreadLocalRegion Memory Layout (non-linear, for visual reference only) +
+ + {/* SVG Chart - Seamless, maintains aspect ratio */} +
+ + {/* Main Memory Layout 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 ( + + {/* Background (fill only) */} + {isFirst || isLast ? ( + + ) : ( + + )} + + {/* Stroke (no inner strokes) */} + + + {segment.id === "conflict" ? ( + + {segment.label} + + ) : ( + <> + + {segment.label} + + + {segment.range} + + + )} + + ); + })} + + + {/* Vertical Guide Lines */} + + + + + + + + + + + + libnx before v4.10.0 + + + + + + + New libnx / Patched / Official software + + + + +
+
+
+ ); +} diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index 03be4dc..1351690 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -1,6 +1,11 @@ import { Card, CardContent } from "../components/ui/Card"; +import { MemoryLayout } from "../components/MemoryLayout"; 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() { return ( @@ -37,6 +42,39 @@ export function AboutPage() { + + +

+ Found Incompatible Homebrew? +

+

+ If you encounter unmaintained and closed-source homebrew that + doesn't work with this patcher, please{" "} + + open an issue on GitHub + {" "} + or{" "} + + DM me on GBATemp + + . +

+

+ Note: I do not offer support for patching + piracy-related homebrew (such as DBI). +

+
+

For Developers

@@ -44,7 +82,7 @@ export function AboutPage() { Do not rely on this tool. Please recompile your homebrew with{" "} libnx v4.10.0 (or newer). -
+

The updated library resolves the memory conflict and stays backward compatible with older Atmosphère versions and @@ -60,13 +98,31 @@ export function AboutPage() {

+ + +

License

+

+ This project is licensed under the GNU General Public License v2.0 + or later (GPL-2.0-or-later). See the{" "} + + LICENSE + {" "} + file for details. +

+
+

TL;DR

- Atmosphère 1.10.0+ integrates kernel optimizations from Firmware - 21.0.0. This improves performance but breaks older homebrew, - causing crashes or hangs on exit. If your homebrew is + Atmosphère integrates kernel optimizations from Firmware 21.0.0. + This improves performance but breaks older homebrew compiled with + 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 .nro files. If the app is still active, ask the developer to update it.

@@ -77,21 +133,59 @@ export function AboutPage() { How this tool works

- This tool scans your .nro files to see if they rely - on the old, now-unsafe memory layout. If it detects a conflict, it - modifies the binary to{" "} + This tool scans your .nro files to see if they + contain old libnx code that writes to the conflicting memory + region. If it detects a conflict, it patches certain{" "} - move its Thread Local Storage (TLS) + libnx functions to move their TLS writes {" "} - from offset 0x108 to the safe offset{" "} - 0x180. Effectively, it mimics the result of - recompiling the app with the latest libraries, restoring stability - without needing the source code. + from the conflicting offset 0x108 to the safe offset{" "} + 0x180.

+

+ Note that the patches applied by this tool do not{" "} + move the ThreadVars 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). +

+
+ +
+

+ Why the breakage happened +

+
+

+ The Thread Local Region is 0x200 bytes large. The + first 0x180 bytes are effectively reserved for the + kernel, while the last 0x80 bytes ( + 0x180-0x200) are for userland. Official software + respects this split, placing its TLS data at offset{" "} + 0x180. +

+

+ Previous versions of libnx chose to start user TLS + slots at 0x108 (encroaching on the kernel's area) + to maximize the number of available slots. This was safe until + Firmware 21.0.0, where Nintendo added{" "} + cache_maintenance_flag at 0x104 and{" "} + thread_cpu_time at 0x108. +

+

+ Now, the kernel writes to 0x108 on every thread + switch, overwriting the TLS data that old homebrew stored there. + This corruption causes the crashes. +

+ +
@@ -102,18 +196,18 @@ export function AboutPage() {

It might seem logical for the Homebrew Loader (hbl) to handle this automatically, but that's out of scope for a - simple program loader. HBL’s 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.

Trying to patch memory offsets on the fly is invasive and - fragile; if HBL guesses wrong, it could corrupt the entire - homebrew environment. Furthermore, baking a patcher into the + fragile; if HBL guesses wrong, it could break otherwise + functioning homebrew. Furthermore, baking a patcher into the loader sets a bad precedent. The maintainers want to encourage proper fixes (recompilation) rather than relying on permanent "dirty hacks" within the OS itself.{" "} @@ -123,28 +217,6 @@ export function AboutPage() {

-
-

- Why the breakage happened -

-
-

- 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 (usertls + 0x108) on - every thread switch. -

-

- Previous versions of libnx (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 kernel—and by extension, - Atmosphère—actively uses that space, old homebrew gets its - memory overwritten, leading to immediate crashes. -

-
-
-

Why Atmosphère didn't add a workaround @@ -157,13 +229,20 @@ export function AboutPage() {

While the developers have added quiet workarounds in the past - (like the FW 19.0.0 debug flags), this specific issue was too - risky to mask. A workaround here would require adding - conditional logic to the kernel scheduler—the 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. + (like the{" "} + + FW 19.0.0 debug flags + + ), this specific issue was too risky to mask. A workaround here + would require adding conditional logic to the kernel + scheduler—the 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.