From dcf6212ddd5416e4f030aaea3710fb4f54825435 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:05:08 -0400 Subject: [PATCH] nx2: video decoder service protocol (no decoding yet) Read from the SDK's nn::codec VP9 decoder: the decoder service session (vdec TIPC 1) takes 200 initialize (work buffer transfer memory), 101 configure (X: codec configuration, width, height), 3 its submission event (copy handle at word 3), 6/207/8 start/stop/flush, 115 finalize, 311/314/212 register/release/unregister an output image (reply a 16-byte NativeSyncHandle; all zero reads as signalled), 109 submit a frame (A: bitstream, u64 pts, u8 end of stream) and 213 the next finished frame (image id, size, colour space, pts, end of stream). Output images come from gsv 911, the same request as 429. The service completes each submitted frame at once into the next free image (no picture yet) and keeps its event signalled. Title 1 now sets the decoder up without aborting; in the runs so far it has not submitted frames yet. Auto-stubbed requests on nx2:vdec* sessions log 48 words. --- src/core/hle/service/nx2_gsv.h | 227 ++++++++++++++++++++++++++++--- src/core/hle/service/service.cpp | 4 +- 2 files changed, 209 insertions(+), 22 deletions(-) diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 428470dfcc..0e64b0960f 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -103,26 +104,6 @@ public: explicit INx2NvsrvSession(Core::System& system_) : ServiceFramework{system_, "nx2:nvsrv"} {} }; -// Placeholder for the Switch 2 video decoder (nn::codec::video::detail::ipc), handed out by gsv -// 902 and 1002 (two sessions; `name` tells them apart in logs). TIPC 1 is -// CreateVideoDecoderService (ipc::CreateVideoDecoderService): handle at word 3, result at word -// 4, a session the decoders then talk to. Everything else is auto-stubbed through the nx2: prefix. -class INx2VideoSession final : public ServiceFramework { -public: - explicit INx2VideoSession(Core::System& system_, const char* name) - : ServiceFramework{system_, name} { - RegisterHandlersTipc({ - {1, &INx2VideoSession::CreateDecoderService, "CreateVideoDecoderService"}, - }); - } - -private: - void CreateDecoderService(HLERequestContext& ctx) { - LOG_WARNING(Service, "(STUBBED) {} TIPC 1, returning a video decoder service session", GetServiceName()); - ReturnRawSession(system, ctx, std::make_shared(system, "nx2:vdecsvc")); - } -}; - // Placeholder for the Switch 2 syncpoint server (nn::syncpt), handed out by gsv 202. TIPC 1 // returns a u32 the SDK requires to be < 0x400 and then passes min(value, 63) to TIPC 21. // Everything else is auto-stubbed through the nx2: prefix. @@ -1028,6 +1009,207 @@ private: std::shared_ptr> m_capture_pending = std::make_shared>(false); }; +// The Switch 2 video decoder (nn::codec::video::detail::ipc). gsv 902/1002 hand out a session +// whose TIPC 1 (CreateVideoDecoderService: handle at word 3, result at word 4) creates a decoder +// service, another session of this class. Layouts read from the SDK's nn::codec VP9 decoder: +// - 200(copy handle: work buffer transfer memory, u64 size, u32, u32, u32): initialize. +// - 101(X: codec configuration, u32 0, u32 width, u32 height, u32 rate, u32 max input): configure. +// - 3(): an event (copy handle at word 3, result at word 4) the client attaches as its +// submission-ready event. +// - 6 / 207 / 8: start / stop / flush; 115: finalize. +// - 311(u32 image id): register an output image (gsv 911 creates it); reply a 16-byte +// NativeSyncHandle, all zero reading as already signalled. The client acquires output images +// in registration/release order from a ring of its own (no IPC). +// - 314(u32 image id, u32, NativeSyncHandle): give an output image back; reply a new handle. +// - 212(u32 image id): unregister it. +// - 109(A: one compressed frame, u64 pts in us, u8 end of stream): submit input; reply a u64. +// - 213(): the next finished frame (16 words): u32 image id, u32 width, u32 height, u32 width, +// u32 height, u32 colour space, u64 pts, u8 end of stream. +// Frames are not decoded yet: each submitted frame completes at once into the next free output +// image, so movies run to their end (with no picture) instead of stalling. +class INx2VideoSession final : public ServiceFramework { +public: + explicit INx2VideoSession(Core::System& system_, std::shared_ptr objects_, + const char* name) + : ServiceFramework{system_, name}, m_context{system_, name}, + m_objects{std::move(objects_)} { + RegisterHandlersTipc({ + {1, &INx2VideoSession::CreateDecoderService, "CreateVideoDecoderService"}, + {3, &INx2VideoSession::GetEvent, "GetEvent"}, + {6, &INx2VideoSession::Succeed, "Start"}, + {8, &INx2VideoSession::Succeed, "Flush"}, + {101, &INx2VideoSession::Configure, "Configure"}, + {109, &INx2VideoSession::SubmitInput, "SubmitInputBuffer"}, + {115, &INx2VideoSession::Succeed, "Finalize"}, + {200, &INx2VideoSession::Succeed, "Initialize"}, + {207, &INx2VideoSession::Succeed, "Stop"}, + {212, &INx2VideoSession::UnregisterBuffer, "UnregisterBuffer"}, + {213, &INx2VideoSession::GetOutputInfo, "GetOutputInfo"}, + {311, &INx2VideoSession::RegisterBuffer, "RegisterBuffer"}, + {314, &INx2VideoSession::ReleaseBuffer, "ReleaseBuffer"}, + }); + } + + ~INx2VideoSession() override { + // The ready timer runs on the HostTiming thread: clear its event under its lock first. + { + std::scoped_lock lk{m_ready->mutex}; + m_ready->event = nullptr; + } + if (m_ready_timer) { + system.CoreTiming().UnscheduleEvent(m_ready_timer); + } + if (m_event != nullptr) { + m_context.CloseEvent(m_event); + } + } + +private: + struct Output { + u32 image; + u64 pts; + bool end; + }; + + void Log(HLERequestContext& ctx, std::string_view what) { + if (m_logged++ < 64) { + const auto* w = ctx.CommandBuffer(); + LOG_INFO(Service, "{} TIPC {} {}: [2]={:#x} [3]={:#x} [4]={:#x} [5]={:#x} [6]={:#x} [7]={:#x}", + GetServiceName(), ctx.GetCommand(), what, w[2], w[3], w[4], w[5], w[6], w[7]); + } + } + + void CreateDecoderService(HLERequestContext& ctx) { + Log(ctx, "create decoder service"); + ReturnRawSession(system, ctx, + std::make_shared(system, m_objects, "nx2:vdecsvc")); + } + + void Succeed(HLERequestContext& ctx) { + Log(ctx, ""); + TipcReplyWords(ctx, {}); + } + + void Configure(HLERequestContext& ctx) { + const auto* w = ctx.CommandBuffer(); + // Words 2-3 are the X descriptor of the configuration data. + m_width = w[5]; + m_height = w[6]; + Log(ctx, "configure"); + TipcReplyWords(ctx, {}); + } + + void GetEvent(HLERequestContext& ctx) { + Log(ctx, "event"); + if (m_event == nullptr) { + m_event = m_context.CreateEvent(fmt::format("{}:Ready", GetServiceName())); + // Ready for input at all times (frames complete as they are submitted). The client + // attaches it auto-clearing, so one signal is consumed by the first wait: re-signal + // every 4 ms; a spurious wake only makes it check again. + { + std::scoped_lock lk{m_ready->mutex}; + m_ready->event = m_event; + } + m_ready_timer = Core::Timing::CreateEvent( + "nx2:vdec ready", + [ready = m_ready, &kernel = system.Kernel()]( + s64, std::chrono::nanoseconds) -> std::optional { + std::scoped_lock lk{ready->mutex}; + if (ready->event != nullptr) { + ready->event->Signal(kernel); + } + return std::nullopt; + }); + constexpr std::chrono::nanoseconds ReadyInterval{std::chrono::milliseconds{4}}; + system.CoreTiming().ScheduleLoopingEvent(ReadyInterval, ReadyInterval, m_ready_timer); + } + m_event->Signal(system.Kernel()); + IPC::ResponseBuilder rb{ctx, 2, 1, 0}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(ctx, m_event->GetReadableEvent()); + } + + void RegisterBuffer(HLERequestContext& ctx) { + Log(ctx, "register output"); + GiveBack(ctx.CommandBuffer()[2]); + TipcReplyWords(ctx, {0, 0, 0, 0}); + } + + void ReleaseBuffer(HLERequestContext& ctx) { + Log(ctx, "release output"); + GiveBack(ctx.CommandBuffer()[2]); + TipcReplyWords(ctx, {0, 0, 0, 0}); + } + + void UnregisterBuffer(HLERequestContext& ctx) { + Log(ctx, "unregister output"); + std::erase(m_free, ctx.CommandBuffer()[2]); + TipcReplyWords(ctx, {}); + } + + // An output image is free again: a frame waiting for one completes into it. + void GiveBack(u32 image) { + if (!m_waiting.empty()) { + m_outputs.push_back({image, m_waiting.front().pts, m_waiting.front().end}); + m_waiting.pop_front(); + return; + } + m_free.push_back(image); + } + + void SubmitInput(HLERequestContext& ctx) { + const auto* w = ctx.CommandBuffer(); + // Words 2-4 are the A descriptor of the compressed frame. + const u64 pts = u64{w[5]} | (u64{w[6]} << 32); + const bool end = (w[7] & 0xFF) != 0; + Log(ctx, "submit"); + if (!m_free.empty()) { + m_outputs.push_back({m_free.front(), pts, end}); + m_free.pop_front(); + } else { + m_waiting.push_back({0, pts, end}); + } + const u64 id = ++m_submitted; + if (m_event != nullptr) { + m_event->Signal(system.Kernel()); + } + TipcReplyWords(ctx, {static_cast(id), static_cast(id >> 32)}); + } + + void GetOutputInfo(HLERequestContext& ctx) { + Log(ctx, "output info"); + if (m_outputs.empty()) { + // Nothing finished: the code the client's own Acquire uses for an empty ring. + IPC::ResponseBuilder rb{ctx, 2}; + rb.PushRaw(0x1c286f); + return; + } + const Output output = m_outputs.front(); + m_outputs.pop_front(); + TipcReplyWords(ctx, {output.image, m_width, m_height, m_width, m_height, 0, + static_cast(output.pts), static_cast(output.pts >> 32), + output.end ? 1U : 0U, 0, 0, 0, 0, 0, 0, 0}); + } + + struct ReadyState { + std::mutex mutex; + Kernel::KEvent* event{}; + }; + + KernelHelpers::ServiceContext m_context; + std::shared_ptr m_objects; + Kernel::KEvent* m_event{}; + std::shared_ptr m_ready = std::make_shared(); + std::shared_ptr m_ready_timer; + u32 m_width{}; + u32 m_height{}; + u64 m_submitted{}; + u32 m_logged{}; + std::deque m_free; + std::deque m_waiting; + std::deque m_outputs; +}; + // Placeholder for the Switch 2 graphics-system server that nn::gsv::BootWithRemoteMemory talks // to over TIPC. Nothing behind it is emulated: commands succeed and are logged, so boot can show // what comes next. Commands 202 and 1402 hand out the syncpt and nvsrv sessions the SDK uses. @@ -1060,6 +1242,8 @@ public: {902, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession (guessed)"}, {1001, &INx2GraphicsSystemSession::Stub, "VideoInitialize2 (guessed)"}, {1002, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession2 (guessed)"}, + // Same request as 429: the image backing a decoder output (nn::codec image pools). + {911, &INx2GraphicsSystemSession::CreateNativeImage, "CreateDecoderImage (guessed)"}, }); } @@ -1101,7 +1285,8 @@ private: LOG_WARNING(Service, "(STUBBED) nx2:gsv TIPC {}, returning a video decoder session", ctx.GetCommand()); ReturnRawSession(system, ctx, - std::make_shared(system, second ? "nx2:vdec2" : "nx2:vdec")); + std::make_shared(system, m_objects, + second ? "nx2:vdec2" : "nx2:vdec")); } void OpenSyncptSession(HLERequestContext& ctx) { diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp index df929e8193..5a779af3ca 100644 --- a/src/core/hle/service/service.cpp +++ b/src/core/hle/service/service.cpp @@ -72,7 +72,9 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx, fmt::memory_buffer buf; fmt::format_to(std::back_inserter(buf), "function '{}({})': port='{}' cmd_buf={{[0]={:#x}", ctx.GetCommand(), function_name, service_name, cmd_buf[0]); - for (int i = 1; i <= 8; ++i) + // The Switch 2 video decoder sessions are being reverse engineered: dump whole requests. + const int last_word = std::string_view{service_name}.starts_with("nx2:vdec") ? 47 : 8; + for (int i = 1; i <= last_word; ++i) fmt::format_to(std::back_inserter(buf), ", [{}]={:#x}", i, cmd_buf[i]); buf.push_back('}');