Homebrew I'm trying to build a Native-res capture program that streams 1080p 60hz video to any PC through USB. This is my progress so far

Papux200

New Member
Newbie
Joined
Sep 11, 2026
Messages
1
Reaction score
0
Trophies
0
Age
21
XP
7
Country
Colombia
Repo: https://github.com/TomasUribe/switch-frame-tap (GPL-2.0)

What I'm trying to build: a homebrew sysmodule that streams the Switch's screen to any PC over a USB cable at the game's native resolution — 1080p, 60 fps — with no capture card and no quality loss. The Switch would effectively be its own capture card: plug in a cable, open a small receiver app on the PC, and get a clean, low-latency 1080p60 feed you could play from, record, or stream.

Right now the best option is SysDVR, which is great but tops out at 720p30. I wanted to see whether that limit could be broken, and if not, understand exactly why. This post is where I've got to so far.

Upfront, because it matters: I built this together with Claude, Anthropic's AI, through Claude Code. Claude wrote nearly all of the code and the docs, did the source-reading, designed the probes and interpreted the logs. I set the direction, ran every hardware test on my own console, and made the calls on what to push and what to drop. This post was drafted with it too. If you'd rather skip AI-assisted work, fair enough — but everything marked "verified on hardware" below really was run on a real console, and the mistakes, including the AI's own bugs that froze my console, are all written down in the repo.

That 720p30 cap (game layer only) isn't SysDVR's fault — it reads grc:d, the game-recording encoder, and the encoder's config is fixed in firmware. So the obvious question is whether you can take the frame earlier: read the game's own swapchain and push it through the Tegra X1's fixed-function blocks yourself.

Claude and I worked that all the way down on a Mariko running 22.5.0 / Atmosphère 1.11.2. This is a work in progress, not a finished streamer — but a few pieces are done and verified on hardware, and the negative results are worth publishing on their own, because we couldn't find them written down anywhere.

What works, on real hardware​


A transparent vi:u mitm that sees every frame at a sustained 60 fps. It wraps GetDisplayServiceIApplicationDisplayServiceGetRelayServiceIHOSBinderDriver and intercepts TransactParcelAuto, invisible to the game. From the binder traffic it recovers the exact layout of every frame: setPreallocatedBuffer carries a flattened NvGraphicBuffer, and queueBuffer names the swapchain slot. For MK8: one nvmap object, 3 × 1920x1080 A8B8G8R8, block-linear kind 0xFE, block_height_log2 4, pitch 7680.

A complete VIC pipeline driven from a sysmodule, byte-exact. The Video Image Compositor is the block that does block-linear → linear, scaling and format conversion in hardware, no GPU involved. Over raw nvdrv ioctls:

Code:
heap alloc -> svcSetMemoryAttribute(Uncached) -> nvmap CREATE/ALLOC
  -> MAP_CMD_BUFFER pin -> host1x cmdbuf (SETCL + methods + INCR_SYNCPT)
  -> CHANNEL_SUBMIT -> syncpoint wait -> cache invalidate -> read back

A fill and a real blit both reproduce their expected output byte for byte. Output order is A,R,G,B (AV_PIX_FMT_ARGB). NVENC opens too.

Four things cost us days each, so in case they save you the same:

  • SETCL is mandatory and nobody tells you. METHOD_OFFSET (0x10) and METHOD_DATA (0x11) are registers of the current host1x class. libdrm never emits SETCL because the DRM kernel driver sets the class itself. nvservices' CHANNEL_SUBMIT does not. Without SETCL(0, 0x5D, 0) every method write lands on meaningless registers — while INCR_SYNCPT (register 0x00, present in every class) still fires, so the job looks like it completed successfully. Six runs.
  • Relocs are inert on Horizon. The command buffer is never patched. Pin with MAP_CMD_BUFFER, inline the returned address, submit with num_relocs = 0.
  • The syncpoint increment has to be in the command stream. syncpt_incrs in the submit only raises the syncpoint's max. Leave out the NONINCR(UCLASS_INCR_SYNCPT, 1) and nvnflinger — which composites on the same VIC syncpoint — waits forever and the console hard-freezes.
  • A zero address doesn't fail politely. The VIC hangs, and a hung VIC takes the compositor with it. Froze my console twice this way, both times in AI-written code — the second time right after the AI had diagnosed exactly that failure from the first.

A libstratosphere patch that upstream doesn't have. 55 lines. Atmosphère's mitm framework can forward commands it doesn't implement, but only for objects on a domain session. vi:u hands out its sub-objects on a non-domain session, so there's nowhere to put the forward service and every undeclared command on a wrapped sub-object fails instead of passing through. If you've ever tried to mitm vi and given up, this is probably why.

Some undocumented vi ABIs, confirmed on hardware: CreateIndirectLayer (2050), CreateIndirectProducerEndPoint (2052), CreateIndirectConsumerEndPoint (2054), all {u64, u64} -> u64. Also GetDisplayService's command id is the service type, not 0 — vi:u=0, vi:s=1, vi:m=2.

What's blocked, and why​


A sysmodule can process frames at full speed. Getting hold of one is the problem. Three routes, each taken to a definite verdict:

  • Import the game's swapchain nvmap handle. FROM_ID succeeds, MAP_CMD_BUFFER returns phys=0 silently and the IOVA allocator doesn't even advance. Survives is_compr, MAP_CMD_BUFFER_EX, relocs, the full 0xFFFFFFFF nvdrv:t permission mask, and the game's exact aruid adopted before any Open. It's structural: at Initialize nvservices is handed CUR_PROCESS_HANDLE and maps client memory through that. The game's pages are in the game's process.
  • vi indirect layers. 0x60A PreconditionViolation. The whole object graph builds and the layer is just empty — wiring an application's layer to an indirect layer is AM's job, and a sysmodule can't drive AM.
  • Display controller readback. No such ioctl exists anywhere in nvdrv. nvdisp-disp0 is FLIP / SET_MODE / GET_WINDOW; nvdcutil is DSI/EDID test plumbing.

(caps CaptureRawImage is [1.0.0], removed long before 22.5.0.)

So we're fairly confident this is why SysDVR is stuck at 720p30. It isn't that nobody tried — the platform doesn't let a sysmodule reach another process's framebuffer through the graphics stack.

Where it is now​


The live route goes around the graphics stack entirely: the kernel debug SVCs, the same ones Atmosphère's own cheat engine uses to read a running game at 60 Hz. Reading mesosphere answers most of it up front — MemoryAttribute_DeviceShared, the thing that killed the nvmap route, isn't consulted by ReadDebugMemory's permission check. That code is written and awaiting its first hardware run.

If you want to fork it​


Everything's GPL-2.0 and the repo is mostly a research log — every dead end is recorded with its evidence specifically so nobody repeats it. Things I'd love help with or would be glad to see someone take further:

  • The debug-SVC capture path, if you've got a console you don't mind freezing.
  • NVENC. The channel opens, the encode isn't written.
  • The 8200-series shared-buffer commands (BindSharedLowLevelLayerToIndirectLayer and friends) — the last speculative avenue for making an indirect layer actually populate.
  • Anyone who knows why nvservices refuses foreign handles at the source level. We inferred the mechanism from behaviour; I'd like to be corrected if it's wrong.

Reach me here, at [email protected], or on GitHub issues.

Usual disclaimer: this interposes on the graphics stack and drives hardware engines directly. It froze my console twice. Don't run it on something you care about, and have a NAND backup.
 

Site & Scene News

Popular threads in this forum