From 587633debecc2402556307d8ec14541f7fd40335 Mon Sep 17 00:00:00 2001 From: PreTenD Date: Wed, 4 Nov 2026 10:35:03 -0600 Subject: [PATCH 1/4] Star Fox: add audio and shader support with optional 60 FPS Implement hwopus command 100 and sysmix playback, extend Ampere lowering, and correct Star Fox window texture alignment. Use format conversion for its known upscale pair and add a build-guarded output-rate option. Assisted-by: OpenAI Codex (implementation, investigation, tests and draft text) --- dist/nx2_switches.txt | 6 + src/core/CMakeLists.txt | 1 + .../audio/hardware_opus_decoder_manager.cpp | 8 + .../audio/hardware_opus_decoder_manager.h | 1 + .../service/audio/nx2_audio_device_manager.h | 140 +++++++-- src/core/hle/service/audio/nx2_hwacod.h | 272 ++++++++++++++++++ src/core/hle/service/nx2_gsv.h | 7 +- src/core/loader/nso.cpp | 34 +++ .../frontend/ampere/lower_to_maxwell.h | 128 ++++++--- src/tests/CMakeLists.txt | 1 + src/tests/video_core/ampere_gradient.cpp | 177 ++++++++++++ src/tests/video_core/ampere_i2f.cpp | 29 ++ src/tests/video_core/ampere_ldc.cpp | 24 ++ src/video_core/engines/kepler_compute.cpp | 52 ++-- 14 files changed, 808 insertions(+), 72 deletions(-) create mode 100644 src/core/hle/service/audio/nx2_hwacod.h create mode 100644 src/tests/video_core/ampere_gradient.cpp diff --git a/dist/nx2_switches.txt b/dist/nx2_switches.txt index 5bcce185e1..7dedcb4579 100644 --- a/dist/nx2_switches.txt +++ b/dist/nx2_switches.txt @@ -23,6 +23,12 @@ EDEN_NX2_FENCE_BATCH=1 #EDEN_NX2_FRAMES_AHEAD=2 # --- Workarounds / Debug --- + +# Star Fox: opt into a 30-60 FPS output interval without accelerating the guest clock. +# Only the tested main build A7730D0F74D168D90422354C8317F76FF78AD938 is patched. +# 60 FPS was tested locally; other levels and updates still need verification. +#EDEN_STARFOX_OUTPUT_FPS=60 + # Terrain/indirect drawing fix for Donkey Kong Bananza: enabled by default. Set to 0 only to leave to GPU. #EDEN_NX2_RESETFIX=0 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1ff9f271d2..a7213df3ce 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -532,6 +532,7 @@ add_library(core STATIC hle/service/audio/audio_renderer_manager.cpp hle/service/audio/audio_renderer_manager.h hle/service/audio/nx2_audio_device_manager.h + hle/service/audio/nx2_hwacod.h hle/service/audio/nx2_audio_user_client.h hle/service/audio/audio_renderer.cpp hle/service/audio/audio_renderer.h diff --git a/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp b/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp index b89e210012..bbc6e6991e 100644 --- a/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp +++ b/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp @@ -6,6 +6,7 @@ #include "core/hle/service/audio/hardware_opus_decoder.h" #include "core/hle/service/audio/hardware_opus_decoder_manager.h" +#include "core/hle/service/audio/nx2_hwacod.h" #include "core/hle/service/cmif_serialization.h" namespace Service::Audio { @@ -26,6 +27,7 @@ IHardwareOpusDecoderManager::IHardwareOpusDecoderManager(Core::System& system_) {7, D<&IHardwareOpusDecoderManager::GetWorkBufferSizeForMultiStreamEx>, "GetWorkBufferSizeForMultiStreamEx"}, {8, D<&IHardwareOpusDecoderManager::GetWorkBufferSizeExEx>, "GetWorkBufferSizeExEx"}, {9, D<&IHardwareOpusDecoderManager::GetWorkBufferSizeForMultiStreamExEx>, "GetWorkBufferSizeForMultiStreamExEx"}, + {100, &IHardwareOpusDecoderManager::OpenHwacodClient, "OpenHwacodClient"}, }; // clang-format on RegisterHandlers(functions); @@ -33,6 +35,12 @@ IHardwareOpusDecoderManager::IHardwareOpusDecoderManager(Core::System& system_) IHardwareOpusDecoderManager::~IHardwareOpusDecoderManager() = default; +void IHardwareOpusDecoderManager::OpenHwacodClient(HLERequestContext& ctx) { + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface(ctx, system); +} + Result IHardwareOpusDecoderManager::OpenHardwareOpusDecoder( Out> out_decoder, OpusParameters params, u32 tmem_size, InCopyHandle tmem_handle) { diff --git a/src/core/hle/service/audio/hardware_opus_decoder_manager.h b/src/core/hle/service/audio/hardware_opus_decoder_manager.h index 4dd211e232..5115cc1b61 100644 --- a/src/core/hle/service/audio/hardware_opus_decoder_manager.h +++ b/src/core/hle/service/audio/hardware_opus_decoder_manager.h @@ -25,6 +25,7 @@ class IHardwareOpusDecoderManager final : public ServiceFramework> out_decoder, OpusParameters params, u32 tmem_size, InCopyHandle tmem_handle); diff --git a/src/core/hle/service/audio/nx2_audio_device_manager.h b/src/core/hle/service/audio/nx2_audio_device_manager.h index 746454660e..675012b0ab 100644 --- a/src/core/hle/service/audio/nx2_audio_device_manager.h +++ b/src/core/hle/service/audio/nx2_audio_device_manager.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,22 +18,23 @@ #include +#include "audio_core/audio_core.h" #include "common/string_util.h" #include "core/core.h" #include "core/core_timing.h" #include "core/hle/kernel/k_event.h" -#include "core/memory.h" +#include "core/hle/service/audio/errors.h" #include "core/hle/service/ipc_helpers.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" +#include "core/memory.h" namespace Service::Audio { // Switch 2 sound mixer streams (nn::audio::sysmix, behind audsmx:u). A stream is a ring buffer in // the game's own memory: CreateStream hands over its address and size (4 KiB aligned), OpenStream // its format and length in frames, and once the streams are engaged the system takes frames out of -// the ring at the stream's sample rate. Eden doesn't mix these into its output yet; it drains the -// rings in real time so the game sees its audio being played (Switch Sports' menu movie feeds one). +// the ring at the stream's sample rate. Supported PCM streams are queued to the normal output sink. // The ring's shared indices, in frames modulo (frame count + 1): the system's read index at // buffer + 0x44, the game's write index at buffer + 0x1004. struct Nx2SysmixStream { @@ -51,6 +53,10 @@ struct Nx2SysmixStream { s32 data_threshold{}; ///< free frames that signal the data event Kernel::KEvent* state_event{}; Kernel::KEvent* data_event{}; + AudioCore::Sink::SinkStream* output{}; + u64 data_offset{0x1040}; + f32 gain{1.0f}; + bool reported_samples{}; }; struct Nx2SysmixStreams { @@ -66,9 +72,10 @@ inline std::shared_ptr Nx2SysmixStreamList() { return list; } -// Switch 2 virtual output devices (nn::audio::ListVirtualOutputDevices and friends), reached through -// the object auddmg:u command 0 returns. Eden plays everything through one stereo 48 kHz output, so -// there is one virtual device. The nx2: name keeps the other commands auto-stubbed and logged. +// Switch 2 virtual output devices (nn::audio::ListVirtualOutputDevices and friends), reached +// through the object auddmg:u command 0 returns. Eden plays everything through one stereo 48 kHz +// output, so there is one virtual device. The nx2: name keeps the other commands auto-stubbed and +// logged. // - 0(u32): u32 count, the device names in the out buffer (0x100 bytes each). // - 100(u32): the default device's name in the out buffer. // - 300(16-byte stream format, name buffer): whether the format is supported (result only). @@ -237,10 +244,12 @@ class INx2AudioDeviceManagerService final : public ServiceFramework { public: - explicit INx2SysmixStreamSession(Core::System& system_, std::shared_ptr streams_, + explicit INx2SysmixStreamSession(Core::System& system_, + std::shared_ptr streams_, std::shared_ptr stream_) - : ServiceFramework{system_, "nx2:audsmx:u:stream"}, service_context{system_, "nx2:audsmx:u:stream"}, - streams{std::move(streams_)}, stream{std::move(stream_)} { + : ServiceFramework{system_, "nx2:audsmx:u:stream"}, + service_context{system_, "nx2:audsmx:u:stream"}, streams{std::move(streams_)}, + stream{std::move(stream_)} { // clang-format off static const FunctionInfo functions[] = { {0, &INx2SysmixStreamSession::OpenStream, "OpenStream"}, @@ -269,6 +278,10 @@ class INx2SysmixStreamSession final : public ServiceFrameworkmutex}; streams->streams.erase(stream->id); + if (stream->output) { + system.AudioCore().GetOutputSink().CloseStream(stream->output); + stream->output = nullptr; + } service_context.CloseEvent(stream->state_event); service_context.CloseEvent(stream->data_event); stream->state_event = nullptr; @@ -287,14 +300,40 @@ class INx2SysmixStreamSession final : public ServiceFramework(); const auto flags = rp.Pop(); LOG_INFO(Service_Audio, - "stream {} open: sample type {}, {} channels, {} Hz, {} frames, prefill {}, flags {:#x}", + "stream {} open: sample type {}, {} channels, {} Hz, {} frames, prefill {}, flags " + "{:#x}", stream->id, sample_type, channels, sample_rate, frames, prefill, flags); + const u32 sample_bytes = sample_type == 5 ? 4 : sample_type == 2 ? 2 : 0; + const u64 data_offset = 0x1040 + ((flags & 1) ? 0x80 : 0); + if (!frames || frames > 48000 || !channels || channels > 8 || + (sample_bytes && + data_offset + u64{frames + 1} * channels * sample_bytes > stream->size)) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultInvalidArgument); + return; + } { std::scoped_lock lk{streams->mutex}; + if (stream->output) { + system.AudioCore().GetOutputSink().CloseStream(stream->output); + stream->output = nullptr; + } stream->sample_type = sample_type; stream->channels = channels; stream->sample_rate = sample_rate; stream->frames = frames; + stream->data_offset = data_offset; + if (sample_bytes && sample_rate == 48000 && + (channels == 1 || channels == 2 || channels == 6)) { + stream->output = system.AudioCore().GetOutputSink().AcquireSinkStream( + system, channels == 1 ? 2 : channels, "NX2 Sysmix", + AudioCore::Sink::StreamType::Out); + stream->output->SetSystemVolume(stream->gain); + } else { + LOG_WARNING(Service_Audio, + "Unsupported sysmix output format: type {} channels {} rate {}", + sample_type, channels, sample_rate); + } stream->owed_frames = 0; stream->open = true; stream->state_event->Signal(system.Kernel()); @@ -307,6 +346,10 @@ class INx2SysmixStreamSession final : public ServiceFrameworkmutex}; stream->open = false; + if (stream->output) { + system.AudioCore().GetOutputSink().CloseStream(stream->output); + stream->output = nullptr; + } stream->state_event->Signal(system.Kernel()); } IPC::ResponseBuilder rb{ctx, 2}; @@ -326,14 +369,19 @@ class INx2SysmixStreamSession final : public ServiceFrameworkmutex}; IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(gain); + rb.Push(stream->gain); } void SetStreamGain(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - gain = rp.Pop(); + const f32 gain = rp.Pop(); + std::scoped_lock lk{streams->mutex}; + stream->gain = std::isfinite(gain) ? std::clamp(gain, 0.0f, 1.0f) : 0.0f; + if (stream->output) + stream->output->SetSystemVolume(stream->gain); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); } @@ -348,7 +396,8 @@ class INx2SysmixStreamSession final : public ServiceFramework(); const auto threshold = rp.Pop(); - LOG_INFO(Service_Audio, "stream {} data event: mode {}, threshold {}", stream->id, mode, threshold); + LOG_INFO(Service_Audio, "stream {} data event: mode {}, threshold {}", stream->id, mode, + threshold); { // The game waits on this before its first write, so signal it at once when the ring // already has room (it is empty right after OpenStream); the drain timer keeps it up. @@ -380,7 +429,6 @@ class INx2SysmixStreamSession final : public ServiceFramework streams; std::shared_ptr stream; - f32 gain{1.0f}; }; // The session audsmx:u command 0 returns. Commands: @@ -389,7 +437,8 @@ class INx2SysmixStreamSession final : public ServiceFramework { public: - explicit INx2SysmixUserSession(Core::System& system_) : ServiceFramework{system_, "nx2:audsmx:u:session"} { + explicit INx2SysmixUserSession(Core::System& system_) + : ServiceFramework{system_, "nx2:audsmx:u:session"} { // clang-format off static const FunctionInfo functions[] = { {1, &INx2SysmixUserSession::CreateStream, "CreateStream"}, @@ -400,8 +449,8 @@ class INx2SysmixUserSession final : public ServiceFramework std::optional { + [streams = streams, &system = system, owner = session_id]( + s64, std::chrono::nanoseconds) -> std::optional { Drain(system, *streams, owner); return std::nullopt; }); @@ -435,12 +484,62 @@ class INx2SysmixUserSession final : public ServiceFrameworkengaged && stream->output && !stream->output->IsPaused()) { + stream->output->Stop(); + stream->output->ClearQueue(); + } if (stream->engaged) { - stream->owed_frames += stream->sample_rate * - std::chrono::duration(DrainInterval).count(); + stream->owed_frames += + stream->sample_rate * std::chrono::duration(DrainInterval).count(); const auto owed = static_cast(stream->owed_frames); stream->owed_frames -= owed; + if (stream->output && stream->output->GetQueueSize() >= 8) + continue; const u32 taken = (std::min)(filled, owed); + if (stream->output && owed) { + const u32 channels = stream->channels; + const u32 output_channels = channels == 1 ? 2 : channels; + std::vector samples(owed * output_channels, 0); + const u32 sample_bytes = stream->sample_type == 5 ? 4 : 2; + std::vector input(taken * channels * sample_bytes); + const u32 first = (std::min)(taken, capacity - read); + const u64 data = stream->buffer + stream->data_offset; + const u64 stride = channels * sample_bytes; + memory.ReadBlock(data + read * stride, input.data(), first * stride); + if (first < taken) { + memory.ReadBlock(data, input.data() + first * stride, + (taken - first) * stride); + } + s32 peak{}; + for (u32 frame = 0; frame < taken; ++frame) { + for (u32 channel = 0; channel < output_channels; ++channel) { + const u32 source = frame * channels + (channels == 1 ? 0 : channel); + s16 sample{}; + if (sample_bytes == 4) { + f32 value{}; + std::memcpy(&value, input.data() + source * 4, 4); + if (std::isfinite(value)) { + const f32 scaled = std::clamp(value, -1.0f, 1.0f) * 32768.0f; + sample = + static_cast(std::clamp(scaled, -32768.0f, 32767.0f)); + } + } else { + std::memcpy(&sample, input.data() + source * 2, 2); + } + samples[frame * output_channels + channel] = sample; + peak = (std::max)(peak, std::abs(static_cast(sample))); + } + } + AudioCore::Sink::SinkBuffer buffer{owed, 0, 0, false}; + stream->output->AppendBuffer(buffer, samples); + stream->output->Start(); + if (peak && !stream->reported_samples) { + stream->reported_samples = true; + LOG_INFO(Service_Audio, + "sysmix stream {} output active: {} frames, PCM16 peak {}", + stream->id, taken, peak); + } + } if (taken != 0) { memory.Write32(stream->buffer + ReadIndexOffset, (read + taken) % capacity); filled -= taken; @@ -495,7 +594,8 @@ class INx2SysmixUserSession final : public ServiceFramework +#include +#include +#include +#include +#include "audio_core/opus/decoder_manager.h" +#include "core/core.h" +#include "core/hle/kernel/k_event.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_transfer_memory.h" +#include "core/hle/service/audio/errors.h" +#include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/memory.h" + +namespace Service::Audio { +using HwacodParameter = std::array; +using HwacodSharedParameter = std::array; + +class IHwacodManager final : public ServiceFramework { +public: + IHwacodManager(Core::System& system_, HwacodSharedParameter layout_, + Kernel::KTransferMemory* transfer_, u64 io_, u64 io_size_) + : ServiceFramework{system_, "IHwacodManager"}, layout{layout_}, transfer{transfer_}, + memory{transfer_->GetOwner()->GetMemory()}, io{io_}, io_size{io_size_}, + context{system_, "IHwacodManager"} { + transfer->Open(system.Kernel()); + event = context.CreateEvent("HwacodCompletion"); + static const FunctionInfo functions[] = { + {0, &IHwacodManager::InitializeInstance, "InitializeInstance"}, + {1, &IHwacodManager::FinalizeInstance, "FinalizeInstance"}, + {10, &IHwacodManager::Execute, "Execute"}, + }; + RegisterHandlers(functions); + } + ~IHwacodManager() override { + for (auto& [id, state] : instances) + opus_decoder_destroy(state.decoder); + context.CloseEvent(event); + transfer->Close(system.Kernel()); + } + Kernel::KReadableEvent& CompletionEvent() { + return event->GetReadableEvent(); + } + +private: + struct Instance { + ::OpusDecoder* decoder; + u32 rate; + u32 channels; + }; + void Reply(HLERequestContext& ctx, Result result) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + } + void InitializeInstance(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u32 id = rp.Pop(); + HwacodParameter p{}; + const auto bytes = ctx.ReadBuffer(); + if (bytes.size() != sizeof(p)) { + Reply(ctx, ResultInvalidArgument); + return; + } + std::memcpy(p.data(), bytes.data(), sizeof(p)); + LOG_INFO(Service_Audio, "hwacod initialize id={} kind={} rate={} channels={}", id, p[0], + p[2], p[3]); + if (id >= layout[14] || p[0] != 1 || p[1] != 0) { + Reply(ctx, ResultNotSupported); + return; + } + int error{}; + auto* decoder = opus_decoder_create(static_cast(p[2]), static_cast(p[3]), &error); + if (!decoder || error != OPUS_OK) { + Reply(ctx, ResultLibOpusBadArg); + return; + } + if (auto it = instances.find(id); it != instances.end()) + opus_decoder_destroy(it->second.decoder); + instances[id] = {decoder, p[2], p[3]}; + Reply(ctx, ResultSuccess); + } + void FinalizeInstance(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto id = rp.Pop(); + if (auto it = instances.find(id); it != instances.end()) { + opus_decoder_destroy(it->second.decoder); + instances.erase(it); + } + Reply(ctx, ResultSuccess); + } + void Execute(HLERequestContext& ctx) { + const auto tasks = ctx.ReadBuffer(); + const u32 capacity = layout[17] + 1; + const u64 ring_offset = layout[11]; + const u64 data_offset = layout[9]; + const u32 input_stride = layout[15], output_stride = layout[16]; + if (tasks.size() % 16 || capacity < 2 || capacity > 4096 || ring_offset > io_size || + 128ULL + capacity * 64ULL > io_size - ring_offset) { + Reply(ctx, ResultInvalidArgument); + return; + } + const u64 ring = io + ring_offset; + u32 write = memory.Read32(ring + 4); + const u32 read = memory.Read32(ring + 0x44); + if (write >= capacity || read >= capacity || + tasks.size() / 16 > (read + capacity - write - 1) % capacity) { + Reply(ctx, ResultBufferCountReached); + return; + } + for (size_t offset = 0; offset < tasks.size(); offset += 16) { + u64 header{}; + u32 length{}; + std::memcpy(&header, tasks.data() + offset, 8); + std::memcpy(&length, tasks.data() + offset + 8, 4); + const u32 id = (header >> 16) & 0xffff, slot = (header >> 32) & 0xffff; + Result result = ResultSuccess; + u32 output_bytes{}; + const auto it = instances.find(id); + const u64 slot_offset = data_offset + u64{slot} * (input_stride + u64{output_stride}); + if (it == instances.end() || slot >= layout[17] || length > input_stride || + slot_offset > io_size || + input_stride + u64{output_stride} > io_size - slot_offset) { + result = ResultInvalidArgument; + } else { + auto& state = it->second; + const u32 max_frames = std::min(5760U, output_stride / (2 * state.channels)); + if (!max_frames || length > 0x10000) { + result = ResultInvalidArgument; + } else { + std::vector packet(length); + memory.ReadBlock(io + slot_offset, packet.data(), packet.size()); + std::vector pcm(max_frames * state.channels); + // Hardware Opus packets carry an eight-byte big-endian size/range header. + const u8* data = packet.data(); + u32 count = length; + if (length >= 8) { + const u32 framed = (u32{data[0]} << 24) | (u32{data[1]} << 16) | + (u32{data[2]} << 8) | data[3]; + if (framed == length - 8) { + data += 8; + count -= 8; + } + } + if (tasks[offset + 12]) + opus_decoder_ctl(state.decoder, OPUS_RESET_STATE); + const int frames = opus_decode(state.decoder, data, static_cast(count), + pcm.data(), static_cast(max_frames), 0); + if (frames < 0) + result = ResultLibOpusInvalidPacket; + else { + output_bytes = static_cast(frames) * state.channels * 2; + memory.WriteBlock(io + slot_offset + input_stride, pcm.data(), + output_bytes); + } + } + } + if (result != ResultSuccess) { + LOG_ERROR(Service_Audio, + "hwacod task failed instance={} slot={} input_bytes={} result={:#x}", id, + slot, length, result.raw); + } + std::array completion{}; + std::memcpy(completion.data(), &header, 8); + std::memcpy(completion.data() + 16, &output_bytes, 4); + const u32 raw = result.raw; + std::memcpy(completion.data() + 20, &raw, 4); + memory.WriteBlock(ring + 128 + u64{write} * 64, completion.data(), completion.size()); + write = (write + 1) % capacity; + } + memory.Write32(ring + 4, write); + event->Signal(system.Kernel()); + Reply(ctx, ResultSuccess); + } + HwacodSharedParameter layout; + Kernel::KTransferMemory* transfer; + Core::Memory::Memory& memory; + u64 io, io_size; + KernelHelpers::ServiceContext context; + Kernel::KEvent* event{}; + std::map instances; +}; + +class IHwacodClient final : public ServiceFramework { +public: + explicit IHwacodClient(Core::System& system_) + : ServiceFramework{system_, "IHwacodClient"}, impl{system_} { + static const FunctionInfo functions[] = { + {0, &IHwacodClient::InstanceSize, "GetInstanceSize"}, + {1, &IHwacodClient::TaskSize, "GetTaskBufferSize"}, + {10, &IHwacodClient::OpenManager, "OpenManager"}, + }; + RegisterHandlers(functions); + } + +private: + void InstanceSize(HLERequestContext& ctx) { + HwacodParameter p{}; + const auto bytes = ctx.ReadBuffer(); + if (bytes.size() != sizeof(p)) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultInvalidArgument); + return; + } + std::memcpy(p.data(), bytes.data(), sizeof(p)); + u32 size{}; + Result result = ResultNotSupported; + if (p[0] == 1 && !p[1]) { + AudioCore::OpusDecoder::OpusParametersEx params{}; + params.sample_rate = p[2]; + params.channel_count = p[3]; + params.use_large_frame_size = true; + result = impl.GetWorkBufferSizeEx(params, size); + } + LOG_INFO(Service_Audio, "hwacod instance size kind={} rate={} channels={} size={}", p[0], + p[2], p[3], size); + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(result); + rb.Push(size); + } + void TaskSize(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u32 count = rp.Pop(); + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(count > 0 && count < 4096 ? ResultSuccess : ResultInvalidArgument); + rb.Push(count > 0 && count < 4096 ? 128 + (count + 1) * 16 : 0U); + } + void OpenManager(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto layout = rp.PopRaw(); + const u64 transfer_size = rp.Pop(), io = rp.Pop(), io_size = rp.Pop(); + const u32 flags = rp.Pop(); + auto transfer = ctx.GetObjectFromHandle(ctx.GetCopyHandle(0)); + LOG_INFO(Service_Audio, + "hwacod open transfer={:#x} io={:#x} size={:#x} flags={} instances={} tasks={} " + "ring={:#x} input={} output={}", + transfer_size, io, io_size, flags, layout[14], layout[17], layout[11], layout[15], + layout[16]); + if (transfer.IsNull() || transfer_size > transfer->GetSize() || layout[0] != 1 || + !layout[14] || layout[14] > 4096 || !layout[17] || layout[17] >= 4096 || !io || + io_size == 0 || io_size > 0x10000000 || io + io_size < io || layout[12] > io_size) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultInvalidArgument); + return; + } + auto& memory = transfer->GetOwner()->GetMemory(); + for (u64 offset = 0; offset < io_size; offset += 0x1000) { + if (!memory.IsValidVirtualAddress(io + offset)) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultInvalidAddressInfo); + return; + } + } + if (!memory.IsValidVirtualAddress(io + io_size - 1)) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultInvalidAddressInfo); + return; + } + auto manager = std::make_shared(system, layout, transfer.GetPointerUnsafe(), + io, io_size); + IPC::ResponseBuilder rb{ctx, 3, 1, 1}; + rb.Push(ResultSuccess); + rb.Push(0); + rb.PushCopyObjects(ctx, manager->CompletionEvent()); + rb.PushIpcInterface(ctx, manager); + } + AudioCore::OpusDecoder::OpusDecoderManager impl; +}; +} // namespace Service::Audio diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index e2060c90a7..eb85d90c01 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -3312,7 +3312,12 @@ class INx2GraphicsSystemSession final : public ServiceFramework size={:#x} align={:#x}", w[3], layout.width, layout.height, layout.block_height_log2, layout.pitch, w[8], layout.size, layout.alignment); diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index 4b9c89d38e..474f733094 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -5,7 +5,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include +#include #include #include #include @@ -177,6 +179,38 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), patchable_section.data()); } + // Opt-in Star Fox output pacing for the tested build. The engine's explicit-rate path + // updates its render delta as well; simulation timing and the guest clock stay unchanged. + if (const char* value = std::getenv("EDEN_STARFOX_OUTPUT_FPS"); value && name == "main" && + process.GetProgramId() == 0x0400E88025B1A000ULL) { + u32 fps{}; + const auto* end = value + std::strlen(value); + const auto parsed = std::from_chars(value, end, fps); + constexpr size_t Offset = 0xB444ADC; + constexpr std::array Original{0xBD4223E1, 0x1E202028, 0x540000ED}; + constexpr std::array BuildId{ + 0xA7, 0x73, 0x0D, 0x0F, 0x74, 0xD1, 0x68, 0xD9, 0x04, 0x22, + 0x35, 0x4C, 0x83, 0x17, 0xF7, 0x6F, 0xF7, 0x8A, 0xD9, 0x38}; + const auto& text = nso_header.segments[0]; + if (parsed.ec == std::errc{} && parsed.ptr == end && fps >= 30 && fps <= 60 && + std::equal(BuildId.begin(), BuildId.end(), nso_header.build_id.begin()) && + Offset >= text.location && Offset + sizeof(Original) <= u64{text.location} + text.size && + module_start + Offset + sizeof(Original) <= codeset.memory.size() && + std::memcmp(codeset.memory.data() + module_start + Offset, Original.data(), + sizeof(Original)) == 0) { + const std::array replacement{ + 0x52800019U | (fps << 5), // mov w25, #fps + 0x1E2E1000, // fmov s0, #1.0 + 0x14000035}; // b main+0xB444BB8 (explicit output-rate path) + std::memcpy(codeset.memory.data() + module_start + Offset, replacement.data(), + sizeof(replacement)); + LOG_INFO(Loader, "Star Fox output interval: {} FPS; guest clock unchanged", fps); + } else { + LOG_WARNING(Loader, + "Star Fox output patch skipped: unsupported build, instructions or FPS (30-60)"); + } + } + // Mario Kart World selects a 1- or 2-vblank frame mode in its main loop. Force its native // 30 FPS mode, or let it choose dynamically using the whole frame time (CPU + GPU wait). // Match the instruction sequence instead of relying on a title-version-specific address. diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 66748f2bcd..5e7766b5a5 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -182,7 +182,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const u32 op = instruction.Bits(0, 12); uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0x896 || op == 0xf60 || op == 0xc11 || op == 0xd08 || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || (op == 0x988 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0xf63 || op == 0xf66 || op == 0xf69 || op == 0xc0b || op == 0x8a5 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)) || op == 0xc0a || - (op == 0x83b && instruction.Bits(91, 1)); + op == 0xf6c || op == 0x4a4 || (op == 0x83b && instruction.Bits(91, 1)); } const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; }; const auto fail = [&](u32 index, std::string_view reason) { @@ -277,7 +277,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 bool rewritten_uniform = false; bool uniform_guard = false; u32 uniform_output = 7; // ULOP3's UPn output: computed in P(6-n), then merged into R239 - if (opcode == 0x2a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 || + if (opcode == 0x2a4 || opcode == 0x4a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 || opcode == 0x292 || opcode == 0x892 || opcode == 0x291 || opcode == 0x896 || opcode == 0x8a5) { // A guarded ULEA without .HI or a carry out lowers to one guarded ISCADD, which // leaves R253 to hold the guest predicate (Pokemon Pokopia's fullscreen passes). @@ -313,7 +313,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 uniform_output = insn.Bits(81, 3); insn.words[2] = (insn.words[2] & ~(7U << 17)) | ((6 - uniform_output) << 17); } - opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 + opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x4a4 ? 0x424 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 : opcode == 0x896 ? 0x816 : opcode == 0x8a5 ? 0x825 : 0x210; } @@ -370,7 +370,8 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } // Pd = LUT(a, b, c) over predicate operands (index | negate << 3): each input becomes // 0 or ~0 (SEL), and LOP3 sets Pd when the result is nonzero. - const auto predicate_lut = [&](u32 lut, u32 a, u32 b, u32 c, u32 target, bool uniform_c = false) { + const auto predicate_lut = [&](u32 lut, u32 a, u32 b, u32 c, u32 target, bool uniform_c = false, + u32 second_target = 7, u32 second_lut = 0) { const u32 inputs[3]{a, b, c}; for (u32 i = 0; i < 3; ++i) { if (i == 2 && uniform_c) { @@ -383,14 +384,28 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 emit(0x5c10000000000000ULL | predicate | 252 | (u64{255} << 8) | (u64{252} << 20) | (u64{1} << 48)); } - if (c & 8) lut = ((lut & 0x55) << 1) | ((lut & 0xaa) >> 1); + if (c & 8) { + lut = ((lut & 0x55) << 1) | ((lut & 0xaa) >> 1); + second_lut = ((second_lut & 0x55) << 1) | ((second_lut & 0xaa) >> 1); + } continue; } emit(0x38a0000000000000ULL | predicate | (250 + i) | (u64{255} << 8) | (u64{0x7ffff} << 20) | (u64{1} << 56) | (u64{inputs[i] ^ 8U} << 39)); } - emit(0x5be0000000000000ULL | predicate | 255 | (u64{250} << 8) | (u64{251} << 20) | - (u64{lut} << 28) | (u64{3} << 36) | (u64{252} << 39) | (u64{target} << 48)); + const auto write_lut = [&](u32 table, u32 destination) { + if (destination == 7) return; + emit(0x5be0000000000000ULL | predicate | 255 | (u64{250} << 8) | (u64{251} << 20) | + (u64{table} << 28) | (u64{3} << 36) | (u64{252} << 39) | (u64{destination} << 48)); + }; + // Both results use the saved inputs. Write an execution-guard predicate last. + if (((predicate >> 16) & 7) == target) { + write_lut(second_lut, second_target); + write_lut(lut, target); + } else { + write_lut(lut, target); + write_lut(second_lut, second_target); + } }; const auto mov_imm = [&](u32 reg, u32 value) { emit(0x0100000000000000ULL | predicate | reg | (u64{15} << 12) | (u64{value} << 20)); @@ -934,10 +949,16 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } case 0x81c: { // PLOP3.LUT; bit 67 makes the third source a uniform predicate. const u32 target = insn.Bits(81, 3); + const u32 second_target = insn.Bits(84, 3); const u32 lut = insn.Bits(64, 3) | (insn.Bits(72, 5) << 3); - if (target == 7 || insn.Bits(84, 3) != 7 || - insn.Bits(16, 8) || insn.words[1]) + if (insn.words[1] || (target != 7 && target == second_target)) return fail(index, "native predicate LUT mode"); + if (target == 7 && second_target == 7) break; + if (second_target != 7) { + predicate_lut(lut, insn.Bits(87, 4), insn.Bits(77, 4), insn.Bits(68, 4), target, + insn.Bits(67, 1) != 0, second_target, insn.Bits(16, 8)); + break; + } if (!insn.Bits(67, 1) && (lut == 0xfe || lut == 0xa8)) { emit(0x5090000000000000ULL | predicate | (u64{target} << 3) | 7 | (u64{insn.Bits(87, 4)} << 12) | (u64{1} << 24) | @@ -1002,14 +1023,15 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 emit(0x5c98000000000000ULL | predicate | target | (u64{source} << 20) | (u64{15} << 39)); break; } - case 0xab9: case 0xabb: { // ULDC U32/U64, direct or indexed. + case 0xab9: case 0xabb: { // ULDC, direct or indexed, with the same widths as LDC. const u32 uniform_dest = insn.Bits(16, 8), index_reg = insn.Bits(24, 8); const u32 width = insn.Bits(73, 3); const u32 target = uniform_register(uniform_dest); const u32 index_target = opcode == 0xabb ? uniform_register(index_reg) : 255U; - if (target == ~0U || target == 255 || (width != 4 && width != 5) || + const u32 alignment = width < 4 ? 1U << (width / 2) : 4U; + if (target == ~0U || target == 255 || width > 5 || (width == 5 && ((uniform_dest & 1) || uniform_register(uniform_dest + 1) != target + 1)) || - bank() >= 18 || insn.Bits(38, 16) % 4 || insn.Bits(62, 2) || + bank() >= 18 || insn.Bits(38, 16) % alignment || insn.Bits(62, 2) || (insn.words[2] & ~0xe00U) != (opcode == 0xab9 ? 0U : 0x08000000U) || index_target == ~0U) return fail(index, "native uniform constant load mode"); @@ -1083,7 +1105,9 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 (u64{insn.Bits(64, 8)} << 20) | (u64{maxwell} << 28)); break; } - case 0xb6c: { // TXD 2D or ARRAY_2D, any channels; the two destination pairs anywhere. + case 0xb6c: case 0xf6c: { // TXD 2D/ARRAY_2D; 0xf6c has a uniform texture handle. + const bool bindless = opcode == 0xf6c; + const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 coordinates = insn.Bits(24, 8), derivatives = insn.Bits(32, 8); // Ampere dimensions: 1 = 2D, 5 = ARRAY_2D; Maxwell TXD's are 2 and 3. The coordinates @@ -1094,23 +1118,33 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // the channel mask: the first two enabled channels go to Rd, Rd+1, the others to the // second destination (RZ drops them; a third channel alone may be in an odd register). const u32 mask = insn.Bits(72, 4), channels = static_cast(std::popcount(mask)); - if ((insn.words[2] & ~(0xfffU | (1U << 26))) != 0x001e0000 || mask == 0 || insn.Bits(59, 2) || - (dimension != 1 && dimension != 5) || binding >= 18 || offset > 0x1fff || + if ((insn.words[2] & ~(0xfffU | (1U << 26))) != (bindless ? 0x081e0000U : 0x001e0000U) || mask == 0 || insn.Bits(59, 2) || + (dimension != 1 && dimension != 5) || + (bindless ? (handle >= 248 || insn.Bits(46, 13) || derivatives > 244 || coordinates > 245) + : (binding >= 18 || offset > 0x1fff)) || (channels > 1 && (dest & 1)) || dest >= 248 || (channels <= 2 && high != 255) || (high != 255 && ((channels > 3 && (high & 1)) || high >= 248)) || coordinates >= 248 || derivatives >= 247) return fail(index, "native gradient texture mode"); - if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + if (!bindless && result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native texture descriptor buffer conflict"); - result.required_texture_buffer = binding; + if (!bindless) result.required_texture_buffer = binding; // Maxwell writes the channels to consecutive registers: when the second destination // doesn't follow Rd (Splatoon Raiders: R12/R18, R18/R44), write them to scratch and move // each into place. - const bool consecutive = channels <= 2 || high == dest + 2; + const bool consecutive = !bindless && (channels <= 2 || high == dest + 2); const u64 target = consecutive ? dest : 248; - emit(0xde00000000000000ULL | predicate | target | (u64{coordinates} << 8) | + if (bindless) { + // Maxwell TXD.B expects the handle before x, y and the optional array layer. + emit(0x5c98000000000000ULL | predicate | 248 | (u64{15} << 39) | (u64{handle} << 20)); + for (u32 i = 0; i < (dimension == 5 ? 3U : 2U); ++i) { + emit(0x5c98000000000000ULL | predicate | (249 + i) | (u64{15} << 39) | + (u64{coordinates + i} << 20)); + } + } + emit((bindless ? 0xde40000000000000ULL : 0xde00000000000000ULL) | predicate | target | (u64{bindless ? 248U : coordinates} << 8) | (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{mask} << 31) | - (u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); + (bindless ? 0ULL : u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); if (!consecutive) { for (u32 i = 0; i < channels; ++i) { if (i >= 2 && high == 255) break; @@ -1327,19 +1361,31 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); break; } - case 0x59f: { // SURED.D.2D.ADD.STRONG.GPU.IGN U32, direct descriptor (title 1's race compute). + case 0x593: // SUATOM.D.2D/3D.ADD/OR.STRONG.GPU.IGN U32, returning the old value. + case 0x59f: { // SURED.D.2D/3D.ADD/OR.STRONG.GPU.IGN U32, direct descriptor. const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8); - if (insn.words[2] != 0x0010e000 || insn.Bits(61, 3) != 3 || binding >= 18 || - offset > 0x1fff || insn.Bits(59, 2) || coordinate > 253) + const u32 dimension = insn.Bits(61, 3); + const bool returns_value = opcode == 0x593; + const u32 operation = insn.Bits(87, 3); // ADD = 0, OR = 6 on both architectures. + const u32 mode = returns_value ? 0x001ee000U : 0x0010e000U; + if ((insn.words[2] & ~0x03800000U) != mode || (operation != 0 && operation != 6) || + (dimension != 3 && dimension != 5) || binding >= 18 || + offset > 0x1fff || insn.Bits(59, 2) || coordinate > (dimension == 5 ? 252U : 253U)) return fail(index, "native surface reduction mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native surface descriptor buffer conflict"); result.required_texture_buffer = binding; - // Maxwell SURED (as the frontend decodes it): operand 0-7, coordinates 8-15, ADD and - // U32 are zero at 24 and 20, type 33 (3 = 2D), bound offset 36, bound 51. + if (returns_value) { + emit(base(0xea00000000000000ULL) | (u64{coordinate} << 8) | + (u64{data} << 20) | (u64{operation} << 29) | (u64{dimension} << 33) | + (u64{offset} << 36)); + break; + } + // Maxwell SURED: operand 0-7, coordinates 8-15, operation 24, U32 = 0 at 20, + // type 33 (3 = 2D, 5 = 3D), bound offset 36, bound 51. emit(0xeb40000000000000ULL | predicate | data | (u64{coordinate} << 8) | - (u64{3} << 33) | (u64{offset} << 36) | (u64{1} << 51)); + (u64{operation} << 24) | (u64{dimension} << 33) | (u64{offset} << 36) | (u64{1} << 51)); break; } case 0x301: // BREV register. @@ -1355,6 +1401,14 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const bool shift_operand = constant ? valid_cbuf() && !insn.Bits(62, 2) : insn.words[1] <= 255; const auto load_shift = [&] { if (constant) materialize(2, 0, 251, false, false); }; const bool right = insn.Bits(76, 1) != 0, high_result = insn.Bits(80, 1) != 0; + if (right && high_result && low == 255 && insn.Bits(73, 2) == 2 && + shift_operand && shift != ~0U && + (insn.words[2] & ~(0x000118ffU | (opcode == 0xc19 ? 0x08000000U : 0U))) == 0x00000400U) { + load_shift(); + emit(base(0x5c28000000000000ULL) | (u64{high} << 8) | (u64{shift} << 20) | + (u64{1} << 48) | (u64{insn.Bits(75, 1)} << 39)); + break; + } // SHF.R.U32 of the low word with a zero high word: a plain logical right shift. if (right && !high_result && high == 255 && insn.Bits(73, 2) == 3 && shift != ~0U && shift_operand && @@ -2158,17 +2212,19 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } break; } - case 0xb63: case 0xf63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. + case 0xb63: case 0xf63: { // TLD4 (gather), 2D/ARRAY_2D; component at bits 87-88. // 0xf63: the handle in a uniform register (bit 91), Maxwell TLD4.B with the handle in Rb. const bool bindless = opcode == 0xf63; const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); const u32 binding = insn.Bits(54, 5); + const bool array = insn.Bits(61, 3) == 5; // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. const bool offsets = insn.Bits(76, 1) != 0; - if (insn.Bits(61, 3) != 1 || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + if ((!array && insn.Bits(61, 3) != 1) || (array && (offsets || (source_a != 255 && source_a >= 248))) || + insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || insn.Bits(89, 1) || mask == 0 || (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) @@ -2180,18 +2236,24 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // with an offset, x and y in Ra, Ra+1 and the offset in Rb. const u32 y = !offsets ? source_b : source_a == 255 ? 255U : source_a + 1; emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); + if (array) { + const u32 x = source_a == 255 ? 255U : source_a + 1; + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{x} << 20)); + emit(0x5c98000000000000ULL | predicate | 252 | (u64{15} << 39) | (u64{y} << 20)); + } else { + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); + } if (offsets) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20)); } if (bindless) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | - (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | + (u64{array ? 3U : 2U} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); } else { emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | - (u64{offsets ? 246U : 255U} << 20) | (u64{2} << 28) | (u64{mask} << 31) | + (u64{offsets ? 246U : 255U} << 20) | (u64{array ? 3U : 2U} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56)); } @@ -2634,7 +2696,9 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // F32), 84-85 the source size (0: 8, 1: 16 bits). const u32 source = opcode == 0xd06 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); const u64 is_signed = insn.Bits(74, 1); - if (opcode == 0x306 && (insn.words[2] & ~0x400U) == 0x00101000 && !(insn.words[1] & 0xcfffff00U)) { + const bool uniform = opcode == 0xd06; + if ((insn.words[2] & ~0x400U) == (uniform ? 0x08101000U : 0x00101000U) && + !(insn.words[1] & 0xefffff00U) && source != ~0U) { // I2F.{U,S}16 Rd, Rs(.H1): Maxwell's 16-bit source format, byte selector 0 or 2. emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{1} << 10) | (is_signed << 13) | (u64{source} << 20) | (u64{insn.Bits(60, 2) * 2} << 41)); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 998a62821a..a617143dca 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(tests video_core/ampere_lowering.cpp video_core/ampere_geometry.cpp video_core/ampere_i2f.cpp + video_core/ampere_gradient.cpp video_core/ampere_returns.cpp video_core/ampere_guarded_ulea.cpp video_core/ampere_compute_lowering.cpp diff --git a/src/tests/video_core/ampere_gradient.cpp b/src/tests/video_core/ampere_gradient.cpp new file mode 100644 index 0000000000..2d12eacc64 --- /dev/null +++ b/src/tests/video_core/ampere_gradient.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include +#include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" + +TEST_CASE("Ampere surface reductions preserve 3D coordinates", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + using Shader::Ampere::LowerToMaxwell; + const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; + // Star Fox: atomic unsigned add of R3 to the 3D image at c[0][0x88], xyz in R0-R2. + Instruction sured{{0x0000759f, 0xa0002203, 0x0010e000, 0x0011e600}}; + for (const u32 dimension : {3U, 5U}) { + sured.words[1] = (sured.words[1] & 0x1fffffffU) | (dimension << 29); + const auto lowered = LowerToMaxwell(std::array{sured, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(lowered.required_texture_buffer == 0); + REQUIRE(lowered.instructions.size() == 2); + const u64 atomic = lowered.instructions[0]; + REQUIRE((atomic & 0xfff0000000000000ULL) == 0xeb40000000000000ULL); + REQUIRE((atomic & 0xff) == 3); // data register + REQUIRE(((atomic >> 8) & 0xff) == 0); // first coordinate + REQUIRE(((atomic >> 16) & 15) == 7); // original guard + REQUIRE(((atomic >> 20) & 7) == 0); // U32 + REQUIRE(((atomic >> 24) & 7) == 0); // ADD + REQUIRE(((atomic >> 33) & 7) == dimension); + REQUIRE(((atomic >> 36) & 0x1fff) == 0x22); // descriptor word offset + REQUIRE(((atomic >> 51) & 1) == 1); // bound descriptor + } + sured.words[0] = 0xfd00759f; // R253-R255 would cross into the zero register in 3D + REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{sured, exit}))); + sured.words[0] = 0x0a00859f; // xyz in R10-R12, guarded by !P0 + auto lowered = LowerToMaxwell(std::array{sured, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(((lowered.instructions[0] >> 8) & 255) == 10); + REQUIRE(((lowered.instructions[0] >> 16) & 15) == 8); + sured = {{0x0000759f, 0xa0002403, 0x0310e000, 0x000fe200}}; + lowered = LowerToMaxwell(std::array{sured, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(((lowered.instructions[0] >> 24) & 7) == 6); // OR + REQUIRE(((lowered.instructions[0] >> 33) & 7) == 5); // still 3D + sured.words[2] = 0x0010e000; + sured.words[2] |= 1U << 23; // unimplemented atomic operation must still be rejected + REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{sured, exit}))); +} + +TEST_CASE("Ampere surface atomic add returns the old 3D texel value", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + using Shader::Ampere::LowerToMaxwell; + const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; + Instruction atomic{{0x00087593, 0xa0002407, 0x001ee000, 0x000ea400}}; + const auto lowered = LowerToMaxwell(std::array{atomic, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(lowered.required_texture_buffer == 0); + REQUIRE(lowered.instructions.size() == 2); + const u64 word = lowered.instructions[0]; + REQUIRE((word & 0xff80000000000000ULL) == 0xea00000000000000ULL); + REQUIRE((word & 255) == 8); // old texel value returned to R8 + REQUIRE(((word >> 8) & 255) == 0); // xyz R0-R2 + REQUIRE(((word >> 20) & 255) == 7); // addend R7 + REQUIRE(((word >> 29) & 15) == 0); // ADD + REQUIRE(((word >> 33) & 7) == 5); // 3D + REQUIRE(((word >> 36) & 0x1fff) == 0x24); // c[0][0x90] + REQUIRE(((word >> 51) & 7) == 0); // U32 + REQUIRE(((word >> 54) & 1) == 0); // bound descriptor + atomic.words[2] &= ~(7U << 17); // writing a fault predicate is not yet supported + REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{atomic, exit}))); +} + +TEST_CASE("Ampere gradient textures accept uniform array handles", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + using Shader::Ampere::LowerToMaxwell; + const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; + // Star Fox's material shader: uniform handle UR4, coordinates R20, derivatives R16. + Instruction txd{{0x14127f6c, 0xa0000410, 0x081e0f10, 0x001f4200}}; + auto lowered = LowerToMaxwell(std::array{txd, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(lowered.required_texture_buffer == ~0U); + REQUIRE(lowered.instructions.size() == 10); + REQUIRE(lowered.instructions[0] == 0x5c9807800dc700f8ULL); + const u64 texture = lowered.instructions[4]; + REQUIRE((texture & 0xffc0000000000000ULL) == 0xde40000000000000ULL); + REQUIRE(((texture >> 8) & 0xff) == 248); + REQUIRE(((texture >> 20) & 0xff) == 16); + REQUIRE(((texture >> 28) & 7) == 3); + REQUIRE(((texture >> 31) & 15) == 15); + REQUIRE((lowered.instructions[5] & 0xff) == 18); + REQUIRE((lowered.instructions[7] & 0xff) == 16); + + txd = {{0x18320f6c, 0xa000041c, 0x0c1e0f34, 0x00414200}}; + lowered = LowerToMaxwell(std::array{txd, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(((lowered.instructions[4] >> 49) & 1) == 1); + REQUIRE(((lowered.instructions[4] >> 16) & 15) == 0); + + txd.words[1] = 0xa0004020; // invalid uniform handle + REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{txd, exit}))); +} + +TEST_CASE("Ampere array gathers preserve the layer and split coordinates", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + const std::array code{{ + {{0x201c7b63, 0xb0000a2b, 0x041e0f16, 0x000f4200}}, + {{0x0000794d, 0, 0x03800000, 0}}, + }}; + const auto lowered = Shader::Ampere::LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + REQUIRE(((lowered.instructions[0] >> 20) & 0xff) == 32); + REQUIRE(((lowered.instructions[1] >> 20) & 0xff) == 33); + REQUIRE(((lowered.instructions[2] >> 20) & 0xff) == 43); + REQUIRE(((lowered.instructions[3] >> 28) & 7) == 3); +} + +TEST_CASE("Ampere signed high-word shifts keep sign extension", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + const std::array code{{ + {{0xff247219, 0x00000025, 0x00011444, 0x000fc600}}, + {{0x0000794d, 0, 0x03800000, 0}}, + }}; + const auto lowered = Shader::Ampere::LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + REQUIRE(((lowered.instructions[0] >> 8) & 0xff) == 68); + REQUIRE(((lowered.instructions[0] >> 20) & 0xff) == 37); + REQUIRE(((lowered.instructions[0] >> 48) & 1) == 1); +} + +TEST_CASE("Ampere uniform multiply accepts an immediate addend", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + const std::array code{{ + {{0x040474a4, 0x00000001, 0x0f8e0205, 0x000fe200}}, + {{0x0000794d, 0, 0x03800000, 0}}, + }}; + const auto lowered = Shader::Ampere::LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + REQUIRE((lowered.instructions[lowered.instructions.size() - 2] & 255) == 220); +} + +TEST_CASE("Ampere dual predicate LUTs read original inputs and preserve their guard", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + for (const u32 guard : {0U, 7U, 8U}) { + const std::array code{{ + {{0x00c8081c | (guard << 12), 0, 0x0010f010, 0x000fc400}}, + {{0x0000794d, 0, 0x03800000, 0}}, + }}; + const auto lowered = Shader::Ampere::LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + for (u32 input = 0; input < 4; ++input) { + std::array predicates{}; + predicates[0] = input & 1; + predicates[1] = input & 2; + predicates[7] = true; + const auto before = predicates; + std::array registers{}; + const auto pred = [&](u32 encoded) { return predicates[encoded & 7] != bool(encoded & 8); }; + for (const u64 word : lowered.instructions) { + if ((word >> 56) == 0xe3) continue; // EXIT + if (!pred((word >> 16) & 15)) continue; + if ((word >> 52) == 0x39a) { + // SEL Rd, RZ, -1, P: select zero when P is true. + registers[word & 255] = !pred((word >> 39) & 15); + } else { + REQUIRE((word >> 52) == 0x5be); + const u32 index = (registers[(word >> 8) & 255] ? 4U : 0U) | + (registers[(word >> 20) & 255] ? 2U : 0U) | + (registers[(word >> 39) & 255] ? 1U : 0U); + const bool value = (word >> (28 + index)) & 1; + const u32 destination = (word >> 48) & 7; + if (destination != 7) predicates[destination] = value; + } + } + const bool active = before[guard & 7] != bool(guard & 8); + const u32 index = (before[0] ? 4U : 0U) | 2U | (before[1] ? 1U : 0U); + REQUIRE(predicates[0] == (active ? bool((0x80 >> index) & 1) : before[0])); + REQUIRE(predicates[1] == (active ? bool((0xc8 >> index) & 1) : before[1])); + } + } +} diff --git a/src/tests/video_core/ampere_i2f.cpp b/src/tests/video_core/ampere_i2f.cpp index eed0388085..d793650091 100644 --- a/src/tests/video_core/ampere_i2f.cpp +++ b/src/tests/video_core/ampere_i2f.cpp @@ -42,4 +42,33 @@ TEST_CASE("Ampere I2F keeps unsigned 16-bit sources unsigned", "[video_core][amp REQUIRE(Contains(lowered, 0x5cb8000000170601ULL)); } +TEST_CASE("Ampere I2F converts 16-bit uniform sources", "[video_core][ampere]") { + // Star Fox's graphics shader: I2F.F32.U16 R2, UR4. + std::array code{{ + {{0x00027d06, 0x00000004, 0x08101000, 0x000e6200}}, + EXIT, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(static_cast(lowered)); + // UR4 is mirrored in R220; preserve unsigned 16-bit conversion. + REQUIRE(Contains(lowered, 0x5cb800000dc70602ULL)); + + code[0] = {{0x00047d06, 0x10000004, 0x08101000, 0x000ef000}}; + const auto upper_half = LowerToMaxwell(code); + REQUIRE(static_cast(upper_half)); + REQUIRE(Contains(upper_half, 0x5cb804000dc70604ULL)); + code[0].words[1] = 0x20000004; + REQUIRE_FALSE(static_cast(LowerToMaxwell(code))); + + code[0] = {{0x00027d06, 0x00000004, 0x08101000, 0x000e6200}}; + + code[0].words[2] |= 0x400; + const auto signed_lowered = LowerToMaxwell(code); + REQUIRE(static_cast(signed_lowered)); + REQUIRE(Contains(signed_lowered, 0x5cb800000dc72602ULL)); + + code[0].words[1] = 64; + REQUIRE_FALSE(static_cast(LowerToMaxwell(code))); +} + } // namespace diff --git a/src/tests/video_core/ampere_ldc.cpp b/src/tests/video_core/ampere_ldc.cpp index 6c365ec578..6c42bf7961 100644 --- a/src/tests/video_core/ampere_ldc.cpp +++ b/src/tests/video_core/ampere_ldc.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include #include @@ -38,3 +39,26 @@ TEST_CASE("Ampere indexed constant loads retain narrow sizes", "[video_core][amp unsupported.words[2] = 1U << 14; REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{unsupported, exit}))); } + +TEST_CASE("Ampere uniform constant loads retain narrow sizes", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + using Shader::Ampere::LowerToMaxwell; + const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; + // Star Fox: ULDC.U16 UR4, c[6][0x20]. + Instruction load{{0x00047ab9, 0x01800800, 0x00000400, 0x000fe200}}; + auto lowered = LowerToMaxwell(std::array{load, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(std::find(lowered.instructions.begin(), lowered.instructions.end(), + 0xef9200600207ffdcULL) != lowered.instructions.end()); + for (u32 width : {0U, 1U, 2U, 3U, 5U}) { + load.words[2] = width << 9; + lowered = LowerToMaxwell(std::array{load, exit}); + REQUIRE(static_cast(lowered)); + REQUIRE(((lowered.instructions[0] >> 48) & 7) == width); + } + load.words[2] = 6U << 9; + REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{load, exit}))); + load.words[2] = 2U << 9; + load.words[1] |= 1U << 6; + REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{load, exit}))); +} diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index dd7a3017ec..f0e3d32c1c 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -895,7 +895,8 @@ bool KeplerCompute::RememberDlssMotionVectors() { // the input colour through cbuf0+0x328 and writes the DLSS output through cbuf0+0x348 (B10G11R11; // 720p to 1440p docked). Blit one to the other with bilinear filtering when both are plain 2D // images of the same format and the output is larger: title 4's slots first, then Fortune's -// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. +// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. Star Fox's +// known 1600x900 RGBA16F -> 1920x1080 RGB10A2 pair also needs format conversion. bool KeplerCompute::UpscaleInsteadOfNativeProgram() { const auto& cbuf = launch_description.const_buffer_config[0]; const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low; @@ -905,19 +906,27 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { return GetTICEntry(tic_index); }; const auto upscale_pair = [](const std::optional& from, - const std::optional& to) { - return from && to && from->format == to->format && from->Address() != 0 && + const std::optional& to, + bool allow_conversion = false) { + return from && to && (from->format == to->format || allow_conversion) && from->Address() != 0 && to->Address() != 0 && to->Width() > from->Width() && from->IsBlockLinear() && to->IsBlockLinear(); }; std::optional input = image(0x90), output = image(0xa0); + const bool starfox_conversion = Settings::GetCurrentProgramID() == 0x0400E88025B1A000ULL && + input && output && input->format == Texture::TextureFormat::R16G16B16A16 && + output->format == Texture::TextureFormat::A2B10G10R10 && + input->Width() == 1600 && input->Height() == 900 && + output->Width() == 1920 && output->Height() == 1080; + bool have_pair = upscale_pair(input, output, starfox_conversion); bool fortunes_weave = false; - if (!upscale_pair(input, output)) { + if (!have_pair) { input = image(0x328); output = image(0x348); fortunes_weave = upscale_pair(input, output); + have_pair = fortunes_weave; } - if (!upscale_pair(input, output)) { + if (!have_pair) { input.reset(); output.reset(); for (u32 first = 0x80; first < 0x120 && !input; first += 4) { @@ -937,15 +946,21 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { if (!input || !output) return false; using RTFormat = Tegra::RenderTargetFormat; - RTFormat format{}; - switch (input->format) { - case Texture::TextureFormat::A2B10G10R10: format = RTFormat::A2B10G10R10_UNORM; break; - case Texture::TextureFormat::A8B8G8R8: format = RTFormat::A8B8G8R8_UNORM; break; - case Texture::TextureFormat::B10G11R11: format = RTFormat::B10G11R11_FLOAT; break; - case Texture::TextureFormat::R16G16B16A16: format = RTFormat::R16G16B16A16_FLOAT; break; - default: return false; - } - const auto surface = [&](const Texture::TICEntry& tic) { + const auto render_format = [](Texture::TextureFormat format) -> std::optional { + switch (format) { + case Texture::TextureFormat::A2B10G10R10: return RTFormat::A2B10G10R10_UNORM; + case Texture::TextureFormat::A8B8G8R8: return RTFormat::A8B8G8R8_UNORM; + case Texture::TextureFormat::B10G11R11: return RTFormat::B10G11R11_FLOAT; + case Texture::TextureFormat::R16G16B16A16: return RTFormat::R16G16B16A16_FLOAT; + default: return std::nullopt; + } + }; + const auto input_format = render_format(input->format); + const auto output_format = render_format(output->format); + if (!input_format || !output_format) { + return false; + } + const auto surface = [&](const Texture::TICEntry& tic, RTFormat format) { Fermi2D::Surface result{}; result.format = format; result.linear = Fermi2D::MemoryLayout::BlockLinear; @@ -1042,7 +1057,7 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { (void)fake; } } - auto source = surface(*input); + auto source = surface(*input, *input_format); // EDEN_NX2_UPSCALE_SOURCE=: blit from another same-sized image (debug aid). if (static const u64 override_source = [] { const char* value = std::getenv("EDEN_NX2_UPSCALE_SOURCE"); @@ -1066,14 +1081,13 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { if (!fortunes_weave || !enabled || !dlss_motion_vectors || dlss_motion_color != input->Address()) return false; - Fermi2D::Surface motion = surface(*dlss_motion_vectors); - motion.format = RTFormat::R16G16_FLOAT; - return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, + Fermi2D::Surface motion = surface(*dlss_motion_vectors, RTFormat::R16G16_FLOAT); + return rasterizer->AccelerateTemporalUpscale(source, surface(*output, *output_format), motion, dlss_depth ? &*dlss_depth : nullptr, dlss_jitter, {1.0f, 1.0f}); }; const bool done = upscale_temporally() || - rasterizer->AccelerateSurfaceCopy(source, surface(*output), config); + rasterizer->AccelerateSurfaceCopy(source, surface(*output, *output_format), config); if (static u32 checked{}; checked++ == sample_at) { const u64 size = u64{output->Width()} * output->Height() * 4; if (const auto device = memory_manager.GpuToCpuAddress(output->Address())) { From 8968377e28670d3fd4bf51a1be47b01d24316895 Mon Sep 17 00:00:00 2001 From: PreTenD Date: Wed, 4 Nov 2026 10:38:20 -0600 Subject: [PATCH 2/4] Star Fox: enable the supported 60 FPS patch automatically Remove the opt-in environment setting while retaining the title, build, instruction and bounds checks. Other games and the guest clock are unchanged. Assisted-by: OpenAI Codex (implementation, tests and draft text) --- dist/nx2_switches.txt | 6 ------ src/core/loader/nso.cpp | 22 ++++++++-------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/dist/nx2_switches.txt b/dist/nx2_switches.txt index 7dedcb4579..5bcce185e1 100644 --- a/dist/nx2_switches.txt +++ b/dist/nx2_switches.txt @@ -23,12 +23,6 @@ EDEN_NX2_FENCE_BATCH=1 #EDEN_NX2_FRAMES_AHEAD=2 # --- Workarounds / Debug --- - -# Star Fox: opt into a 30-60 FPS output interval without accelerating the guest clock. -# Only the tested main build A7730D0F74D168D90422354C8317F76FF78AD938 is patched. -# 60 FPS was tested locally; other levels and updates still need verification. -#EDEN_STARFOX_OUTPUT_FPS=60 - # Terrain/indirect drawing fix for Donkey Kong Bananza: enabled by default. Set to 0 only to leave to GPU. #EDEN_NX2_RESETFIX=0 diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index 474f733094..3f719da360 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -5,9 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include #include -#include #include #include #include @@ -179,35 +177,31 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), patchable_section.data()); } - // Opt-in Star Fox output pacing for the tested build. The engine's explicit-rate path + // Star Fox output pacing for the tested build. The engine's explicit-rate path // updates its render delta as well; simulation timing and the guest clock stay unchanged. - if (const char* value = std::getenv("EDEN_STARFOX_OUTPUT_FPS"); value && name == "main" && - process.GetProgramId() == 0x0400E88025B1A000ULL) { - u32 fps{}; - const auto* end = value + std::strlen(value); - const auto parsed = std::from_chars(value, end, fps); + if (name == "main" && process.GetProgramId() == 0x0400E88025B1A000ULL) { + constexpr u32 Fps = 60; constexpr size_t Offset = 0xB444ADC; constexpr std::array Original{0xBD4223E1, 0x1E202028, 0x540000ED}; constexpr std::array BuildId{ 0xA7, 0x73, 0x0D, 0x0F, 0x74, 0xD1, 0x68, 0xD9, 0x04, 0x22, 0x35, 0x4C, 0x83, 0x17, 0xF7, 0x6F, 0xF7, 0x8A, 0xD9, 0x38}; const auto& text = nso_header.segments[0]; - if (parsed.ec == std::errc{} && parsed.ptr == end && fps >= 30 && fps <= 60 && - std::equal(BuildId.begin(), BuildId.end(), nso_header.build_id.begin()) && + if (std::equal(BuildId.begin(), BuildId.end(), nso_header.build_id.begin()) && Offset >= text.location && Offset + sizeof(Original) <= u64{text.location} + text.size && module_start + Offset + sizeof(Original) <= codeset.memory.size() && std::memcmp(codeset.memory.data() + module_start + Offset, Original.data(), sizeof(Original)) == 0) { - const std::array replacement{ - 0x52800019U | (fps << 5), // mov w25, #fps + constexpr std::array replacement{ + 0x52800019U | (Fps << 5), // mov w25, #60 0x1E2E1000, // fmov s0, #1.0 0x14000035}; // b main+0xB444BB8 (explicit output-rate path) std::memcpy(codeset.memory.data() + module_start + Offset, replacement.data(), sizeof(replacement)); - LOG_INFO(Loader, "Star Fox output interval: {} FPS; guest clock unchanged", fps); + LOG_INFO(Loader, "Star Fox output interval: {} FPS; guest clock unchanged", Fps); } else { LOG_WARNING(Loader, - "Star Fox output patch skipped: unsupported build, instructions or FPS (30-60)"); + "Star Fox output patch skipped: unsupported build or instructions"); } } From 0842962c2506459f549f54f64f6b19003bd9339d Mon Sep 17 00:00:00 2001 From: PreTenD Date: Wed, 4 Nov 2026 11:45:03 -0600 Subject: [PATCH 3/4] ampere: reject TLD4 coordinate pairs crossing scratch registers Check the second coordinate against the dynamic shadow boundary for array and offset gathers. Preserve scalar coordinates and RZ, and cover the reserved-register boundaries in the shader regression tests. Assisted-by: OpenAI Codex (implementation, investigation and tests) --- .../frontend/ampere/lower_to_maxwell.h | 5 ++- src/tests/video_core/ampere_gradient.cpp | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 5e7766b5a5..366de0ca3f 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -2223,9 +2223,10 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. const bool offsets = insn.Bits(76, 1) != 0; - if ((!array && insn.Bits(61, 3) != 1) || (array && (offsets || (source_a != 255 && source_a >= 248))) || + if ((!array && insn.Bits(61, 3) != 1) || (array && offsets) || + ((array || offsets) && source_a != 255 && source_a + 1 >= shadow_boundary) || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || + (offsets && (bindless || source_b == 255)) || insn.Bits(89, 1) || mask == 0 || (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) : (insn.Bits(40, 14) > 0x1fff || binding >= 18 || diff --git a/src/tests/video_core/ampere_gradient.cpp b/src/tests/video_core/ampere_gradient.cpp index 2d12eacc64..42570c8fbc 100644 --- a/src/tests/video_core/ampere_gradient.cpp +++ b/src/tests/video_core/ampere_gradient.cpp @@ -111,6 +111,50 @@ TEST_CASE("Ampere array gathers preserve the layer and split coordinates", "[vid REQUIRE(((lowered.instructions[3] >> 28) & 7) == 3); } +TEST_CASE("Ampere gathers keep coordinate pairs below reserved registers", "[video_core][ampere]") { + using Shader::Ampere::Instruction; + struct Mode { + Instruction instruction; + u32 boundary; + u32 high_uniforms; + bool pair; + }; + const std::array modes{ + Mode{{{0x201c7b63, 0xb0000a2b, 0x041e0f16, 0x000f4200}}, 244, 0, true}, + Mode{{{0xd61c7f63, 0xb000002b, 0x0c1e0f16, 0x000f4200}}, 216, 0, true}, + Mode{{{0xd61c7f63, 0xb000002b, 0x0c1e0f16, 0x000f4200}}, 212, 4, true}, + Mode{{{0x201c7b63, 0x30000a2b, 0x041e1f16, 0x000f4200}}, 244, 0, true}, + Mode{{{0x201c7b63, 0x30000a2b, 0x041e0f16, 0x000f4200}}, 244, 0, false}, + Mode{{{0xd61c7f63, 0x3000002b, 0x0c1e0f16, 0x000f4200}}, 216, 0, false}, + }; + for (const auto& mode : modes) { + for (const u32 source : {mode.boundary - 2, mode.boundary - 1, 255U}) { + CAPTURE(mode.boundary, mode.high_uniforms, mode.pair, source); + auto gather = mode.instruction; + gather.words[0] = (gather.words[0] & 0x00ffffffU) | (source << 24); + const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; + const auto lowered = Shader::Ampere::LowerToMaxwellWith( + std::array{gather, exit}, mode.high_uniforms); + if (mode.pair && source == mode.boundary - 1) { + REQUIRE_FALSE(static_cast(lowered)); + REQUIRE(lowered.error == "native texture gather mode"); + REQUIRE(lowered.instructions.empty()); + continue; + } + REQUIRE(static_cast(lowered)); + REQUIRE(lowered.instructions.size() >= 3); + REQUIRE(((lowered.instructions[0] >> 20) & 255) == source); + if (mode.pair) { + REQUIRE(((lowered.instructions[1] >> 20) & 255) == + (source == 255 ? 255U : source + 1)); + REQUIRE(((lowered.instructions[2] >> 20) & 255) == 43); + } else { + REQUIRE(((lowered.instructions[1] >> 20) & 255) == 43); + } + } + } +} + TEST_CASE("Ampere signed high-word shifts keep sign extension", "[video_core][ampere]") { using Shader::Ampere::Instruction; const std::array code{{ From e87ad310686cd19952b66e6d7ff6cd346d99c21e Mon Sep 17 00:00:00 2001 From: PastaLaPate Date: Fri, 6 Nov 2026 19:02:30 +0100 Subject: [PATCH 4/4] Revert "StarFox compatibility update" --- src/core/CMakeLists.txt | 1 - .../audio/hardware_opus_decoder_manager.cpp | 8 - .../audio/hardware_opus_decoder_manager.h | 1 - .../service/audio/nx2_audio_device_manager.h | 140 ++------- src/core/hle/service/audio/nx2_hwacod.h | 272 ------------------ src/core/hle/service/nx2_gsv.h | 7 +- src/core/loader/nso.cpp | 28 -- .../frontend/ampere/lower_to_maxwell.h | 131 +++------ src/tests/CMakeLists.txt | 1 - src/tests/video_core/ampere_gradient.cpp | 221 -------------- src/tests/video_core/ampere_i2f.cpp | 29 -- src/tests/video_core/ampere_ldc.cpp | 24 -- src/video_core/engines/kepler_compute.cpp | 52 ++-- 13 files changed, 73 insertions(+), 842 deletions(-) delete mode 100644 src/core/hle/service/audio/nx2_hwacod.h delete mode 100644 src/tests/video_core/ampere_gradient.cpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a7213df3ce..1ff9f271d2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -532,7 +532,6 @@ add_library(core STATIC hle/service/audio/audio_renderer_manager.cpp hle/service/audio/audio_renderer_manager.h hle/service/audio/nx2_audio_device_manager.h - hle/service/audio/nx2_hwacod.h hle/service/audio/nx2_audio_user_client.h hle/service/audio/audio_renderer.cpp hle/service/audio/audio_renderer.h diff --git a/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp b/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp index bbc6e6991e..b89e210012 100644 --- a/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp +++ b/src/core/hle/service/audio/hardware_opus_decoder_manager.cpp @@ -6,7 +6,6 @@ #include "core/hle/service/audio/hardware_opus_decoder.h" #include "core/hle/service/audio/hardware_opus_decoder_manager.h" -#include "core/hle/service/audio/nx2_hwacod.h" #include "core/hle/service/cmif_serialization.h" namespace Service::Audio { @@ -27,7 +26,6 @@ IHardwareOpusDecoderManager::IHardwareOpusDecoderManager(Core::System& system_) {7, D<&IHardwareOpusDecoderManager::GetWorkBufferSizeForMultiStreamEx>, "GetWorkBufferSizeForMultiStreamEx"}, {8, D<&IHardwareOpusDecoderManager::GetWorkBufferSizeExEx>, "GetWorkBufferSizeExEx"}, {9, D<&IHardwareOpusDecoderManager::GetWorkBufferSizeForMultiStreamExEx>, "GetWorkBufferSizeForMultiStreamExEx"}, - {100, &IHardwareOpusDecoderManager::OpenHwacodClient, "OpenHwacodClient"}, }; // clang-format on RegisterHandlers(functions); @@ -35,12 +33,6 @@ IHardwareOpusDecoderManager::IHardwareOpusDecoderManager(Core::System& system_) IHardwareOpusDecoderManager::~IHardwareOpusDecoderManager() = default; -void IHardwareOpusDecoderManager::OpenHwacodClient(HLERequestContext& ctx) { - IPC::ResponseBuilder rb{ctx, 2, 0, 1}; - rb.Push(ResultSuccess); - rb.PushIpcInterface(ctx, system); -} - Result IHardwareOpusDecoderManager::OpenHardwareOpusDecoder( Out> out_decoder, OpusParameters params, u32 tmem_size, InCopyHandle tmem_handle) { diff --git a/src/core/hle/service/audio/hardware_opus_decoder_manager.h b/src/core/hle/service/audio/hardware_opus_decoder_manager.h index 5115cc1b61..4dd211e232 100644 --- a/src/core/hle/service/audio/hardware_opus_decoder_manager.h +++ b/src/core/hle/service/audio/hardware_opus_decoder_manager.h @@ -25,7 +25,6 @@ class IHardwareOpusDecoderManager final : public ServiceFramework> out_decoder, OpusParameters params, u32 tmem_size, InCopyHandle tmem_handle); diff --git a/src/core/hle/service/audio/nx2_audio_device_manager.h b/src/core/hle/service/audio/nx2_audio_device_manager.h index 675012b0ab..746454660e 100644 --- a/src/core/hle/service/audio/nx2_audio_device_manager.h +++ b/src/core/hle/service/audio/nx2_audio_device_manager.h @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -18,23 +17,22 @@ #include -#include "audio_core/audio_core.h" #include "common/string_util.h" #include "core/core.h" #include "core/core_timing.h" #include "core/hle/kernel/k_event.h" -#include "core/hle/service/audio/errors.h" +#include "core/memory.h" #include "core/hle/service/ipc_helpers.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/service.h" -#include "core/memory.h" namespace Service::Audio { // Switch 2 sound mixer streams (nn::audio::sysmix, behind audsmx:u). A stream is a ring buffer in // the game's own memory: CreateStream hands over its address and size (4 KiB aligned), OpenStream // its format and length in frames, and once the streams are engaged the system takes frames out of -// the ring at the stream's sample rate. Supported PCM streams are queued to the normal output sink. +// the ring at the stream's sample rate. Eden doesn't mix these into its output yet; it drains the +// rings in real time so the game sees its audio being played (Switch Sports' menu movie feeds one). // The ring's shared indices, in frames modulo (frame count + 1): the system's read index at // buffer + 0x44, the game's write index at buffer + 0x1004. struct Nx2SysmixStream { @@ -53,10 +51,6 @@ struct Nx2SysmixStream { s32 data_threshold{}; ///< free frames that signal the data event Kernel::KEvent* state_event{}; Kernel::KEvent* data_event{}; - AudioCore::Sink::SinkStream* output{}; - u64 data_offset{0x1040}; - f32 gain{1.0f}; - bool reported_samples{}; }; struct Nx2SysmixStreams { @@ -72,10 +66,9 @@ inline std::shared_ptr Nx2SysmixStreamList() { return list; } -// Switch 2 virtual output devices (nn::audio::ListVirtualOutputDevices and friends), reached -// through the object auddmg:u command 0 returns. Eden plays everything through one stereo 48 kHz -// output, so there is one virtual device. The nx2: name keeps the other commands auto-stubbed and -// logged. +// Switch 2 virtual output devices (nn::audio::ListVirtualOutputDevices and friends), reached through +// the object auddmg:u command 0 returns. Eden plays everything through one stereo 48 kHz output, so +// there is one virtual device. The nx2: name keeps the other commands auto-stubbed and logged. // - 0(u32): u32 count, the device names in the out buffer (0x100 bytes each). // - 100(u32): the default device's name in the out buffer. // - 300(16-byte stream format, name buffer): whether the format is supported (result only). @@ -244,12 +237,10 @@ class INx2AudioDeviceManagerService final : public ServiceFramework { public: - explicit INx2SysmixStreamSession(Core::System& system_, - std::shared_ptr streams_, + explicit INx2SysmixStreamSession(Core::System& system_, std::shared_ptr streams_, std::shared_ptr stream_) - : ServiceFramework{system_, "nx2:audsmx:u:stream"}, - service_context{system_, "nx2:audsmx:u:stream"}, streams{std::move(streams_)}, - stream{std::move(stream_)} { + : ServiceFramework{system_, "nx2:audsmx:u:stream"}, service_context{system_, "nx2:audsmx:u:stream"}, + streams{std::move(streams_)}, stream{std::move(stream_)} { // clang-format off static const FunctionInfo functions[] = { {0, &INx2SysmixStreamSession::OpenStream, "OpenStream"}, @@ -278,10 +269,6 @@ class INx2SysmixStreamSession final : public ServiceFrameworkmutex}; streams->streams.erase(stream->id); - if (stream->output) { - system.AudioCore().GetOutputSink().CloseStream(stream->output); - stream->output = nullptr; - } service_context.CloseEvent(stream->state_event); service_context.CloseEvent(stream->data_event); stream->state_event = nullptr; @@ -300,40 +287,14 @@ class INx2SysmixStreamSession final : public ServiceFramework(); const auto flags = rp.Pop(); LOG_INFO(Service_Audio, - "stream {} open: sample type {}, {} channels, {} Hz, {} frames, prefill {}, flags " - "{:#x}", + "stream {} open: sample type {}, {} channels, {} Hz, {} frames, prefill {}, flags {:#x}", stream->id, sample_type, channels, sample_rate, frames, prefill, flags); - const u32 sample_bytes = sample_type == 5 ? 4 : sample_type == 2 ? 2 : 0; - const u64 data_offset = 0x1040 + ((flags & 1) ? 0x80 : 0); - if (!frames || frames > 48000 || !channels || channels > 8 || - (sample_bytes && - data_offset + u64{frames + 1} * channels * sample_bytes > stream->size)) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultInvalidArgument); - return; - } { std::scoped_lock lk{streams->mutex}; - if (stream->output) { - system.AudioCore().GetOutputSink().CloseStream(stream->output); - stream->output = nullptr; - } stream->sample_type = sample_type; stream->channels = channels; stream->sample_rate = sample_rate; stream->frames = frames; - stream->data_offset = data_offset; - if (sample_bytes && sample_rate == 48000 && - (channels == 1 || channels == 2 || channels == 6)) { - stream->output = system.AudioCore().GetOutputSink().AcquireSinkStream( - system, channels == 1 ? 2 : channels, "NX2 Sysmix", - AudioCore::Sink::StreamType::Out); - stream->output->SetSystemVolume(stream->gain); - } else { - LOG_WARNING(Service_Audio, - "Unsupported sysmix output format: type {} channels {} rate {}", - sample_type, channels, sample_rate); - } stream->owed_frames = 0; stream->open = true; stream->state_event->Signal(system.Kernel()); @@ -346,10 +307,6 @@ class INx2SysmixStreamSession final : public ServiceFrameworkmutex}; stream->open = false; - if (stream->output) { - system.AudioCore().GetOutputSink().CloseStream(stream->output); - stream->output = nullptr; - } stream->state_event->Signal(system.Kernel()); } IPC::ResponseBuilder rb{ctx, 2}; @@ -369,19 +326,14 @@ class INx2SysmixStreamSession final : public ServiceFrameworkmutex}; IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(stream->gain); + rb.Push(gain); } void SetStreamGain(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const f32 gain = rp.Pop(); - std::scoped_lock lk{streams->mutex}; - stream->gain = std::isfinite(gain) ? std::clamp(gain, 0.0f, 1.0f) : 0.0f; - if (stream->output) - stream->output->SetSystemVolume(stream->gain); + gain = rp.Pop(); IPC::ResponseBuilder rb{ctx, 2}; rb.Push(ResultSuccess); } @@ -396,8 +348,7 @@ class INx2SysmixStreamSession final : public ServiceFramework(); const auto threshold = rp.Pop(); - LOG_INFO(Service_Audio, "stream {} data event: mode {}, threshold {}", stream->id, mode, - threshold); + LOG_INFO(Service_Audio, "stream {} data event: mode {}, threshold {}", stream->id, mode, threshold); { // The game waits on this before its first write, so signal it at once when the ring // already has room (it is empty right after OpenStream); the drain timer keeps it up. @@ -429,6 +380,7 @@ class INx2SysmixStreamSession final : public ServiceFramework streams; std::shared_ptr stream; + f32 gain{1.0f}; }; // The session audsmx:u command 0 returns. Commands: @@ -437,8 +389,7 @@ class INx2SysmixStreamSession final : public ServiceFramework { public: - explicit INx2SysmixUserSession(Core::System& system_) - : ServiceFramework{system_, "nx2:audsmx:u:session"} { + explicit INx2SysmixUserSession(Core::System& system_) : ServiceFramework{system_, "nx2:audsmx:u:session"} { // clang-format off static const FunctionInfo functions[] = { {1, &INx2SysmixUserSession::CreateStream, "CreateStream"}, @@ -449,8 +400,8 @@ class INx2SysmixUserSession final : public ServiceFramework std::optional { + [streams = streams, &system = system, owner = session_id](s64, std::chrono::nanoseconds) + -> std::optional { Drain(system, *streams, owner); return std::nullopt; }); @@ -484,62 +435,12 @@ class INx2SysmixUserSession final : public ServiceFrameworkengaged && stream->output && !stream->output->IsPaused()) { - stream->output->Stop(); - stream->output->ClearQueue(); - } if (stream->engaged) { - stream->owed_frames += - stream->sample_rate * std::chrono::duration(DrainInterval).count(); + stream->owed_frames += stream->sample_rate * + std::chrono::duration(DrainInterval).count(); const auto owed = static_cast(stream->owed_frames); stream->owed_frames -= owed; - if (stream->output && stream->output->GetQueueSize() >= 8) - continue; const u32 taken = (std::min)(filled, owed); - if (stream->output && owed) { - const u32 channels = stream->channels; - const u32 output_channels = channels == 1 ? 2 : channels; - std::vector samples(owed * output_channels, 0); - const u32 sample_bytes = stream->sample_type == 5 ? 4 : 2; - std::vector input(taken * channels * sample_bytes); - const u32 first = (std::min)(taken, capacity - read); - const u64 data = stream->buffer + stream->data_offset; - const u64 stride = channels * sample_bytes; - memory.ReadBlock(data + read * stride, input.data(), first * stride); - if (first < taken) { - memory.ReadBlock(data, input.data() + first * stride, - (taken - first) * stride); - } - s32 peak{}; - for (u32 frame = 0; frame < taken; ++frame) { - for (u32 channel = 0; channel < output_channels; ++channel) { - const u32 source = frame * channels + (channels == 1 ? 0 : channel); - s16 sample{}; - if (sample_bytes == 4) { - f32 value{}; - std::memcpy(&value, input.data() + source * 4, 4); - if (std::isfinite(value)) { - const f32 scaled = std::clamp(value, -1.0f, 1.0f) * 32768.0f; - sample = - static_cast(std::clamp(scaled, -32768.0f, 32767.0f)); - } - } else { - std::memcpy(&sample, input.data() + source * 2, 2); - } - samples[frame * output_channels + channel] = sample; - peak = (std::max)(peak, std::abs(static_cast(sample))); - } - } - AudioCore::Sink::SinkBuffer buffer{owed, 0, 0, false}; - stream->output->AppendBuffer(buffer, samples); - stream->output->Start(); - if (peak && !stream->reported_samples) { - stream->reported_samples = true; - LOG_INFO(Service_Audio, - "sysmix stream {} output active: {} frames, PCM16 peak {}", - stream->id, taken, peak); - } - } if (taken != 0) { memory.Write32(stream->buffer + ReadIndexOffset, (read + taken) % capacity); filled -= taken; @@ -594,8 +495,7 @@ class INx2SysmixUserSession final : public ServiceFramework -#include -#include -#include -#include -#include "audio_core/opus/decoder_manager.h" -#include "core/core.h" -#include "core/hle/kernel/k_event.h" -#include "core/hle/kernel/k_process.h" -#include "core/hle/kernel/k_transfer_memory.h" -#include "core/hle/service/audio/errors.h" -#include "core/hle/service/ipc_helpers.h" -#include "core/hle/service/kernel_helpers.h" -#include "core/memory.h" - -namespace Service::Audio { -using HwacodParameter = std::array; -using HwacodSharedParameter = std::array; - -class IHwacodManager final : public ServiceFramework { -public: - IHwacodManager(Core::System& system_, HwacodSharedParameter layout_, - Kernel::KTransferMemory* transfer_, u64 io_, u64 io_size_) - : ServiceFramework{system_, "IHwacodManager"}, layout{layout_}, transfer{transfer_}, - memory{transfer_->GetOwner()->GetMemory()}, io{io_}, io_size{io_size_}, - context{system_, "IHwacodManager"} { - transfer->Open(system.Kernel()); - event = context.CreateEvent("HwacodCompletion"); - static const FunctionInfo functions[] = { - {0, &IHwacodManager::InitializeInstance, "InitializeInstance"}, - {1, &IHwacodManager::FinalizeInstance, "FinalizeInstance"}, - {10, &IHwacodManager::Execute, "Execute"}, - }; - RegisterHandlers(functions); - } - ~IHwacodManager() override { - for (auto& [id, state] : instances) - opus_decoder_destroy(state.decoder); - context.CloseEvent(event); - transfer->Close(system.Kernel()); - } - Kernel::KReadableEvent& CompletionEvent() { - return event->GetReadableEvent(); - } - -private: - struct Instance { - ::OpusDecoder* decoder; - u32 rate; - u32 channels; - }; - void Reply(HLERequestContext& ctx, Result result) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(result); - } - void InitializeInstance(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u32 id = rp.Pop(); - HwacodParameter p{}; - const auto bytes = ctx.ReadBuffer(); - if (bytes.size() != sizeof(p)) { - Reply(ctx, ResultInvalidArgument); - return; - } - std::memcpy(p.data(), bytes.data(), sizeof(p)); - LOG_INFO(Service_Audio, "hwacod initialize id={} kind={} rate={} channels={}", id, p[0], - p[2], p[3]); - if (id >= layout[14] || p[0] != 1 || p[1] != 0) { - Reply(ctx, ResultNotSupported); - return; - } - int error{}; - auto* decoder = opus_decoder_create(static_cast(p[2]), static_cast(p[3]), &error); - if (!decoder || error != OPUS_OK) { - Reply(ctx, ResultLibOpusBadArg); - return; - } - if (auto it = instances.find(id); it != instances.end()) - opus_decoder_destroy(it->second.decoder); - instances[id] = {decoder, p[2], p[3]}; - Reply(ctx, ResultSuccess); - } - void FinalizeInstance(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto id = rp.Pop(); - if (auto it = instances.find(id); it != instances.end()) { - opus_decoder_destroy(it->second.decoder); - instances.erase(it); - } - Reply(ctx, ResultSuccess); - } - void Execute(HLERequestContext& ctx) { - const auto tasks = ctx.ReadBuffer(); - const u32 capacity = layout[17] + 1; - const u64 ring_offset = layout[11]; - const u64 data_offset = layout[9]; - const u32 input_stride = layout[15], output_stride = layout[16]; - if (tasks.size() % 16 || capacity < 2 || capacity > 4096 || ring_offset > io_size || - 128ULL + capacity * 64ULL > io_size - ring_offset) { - Reply(ctx, ResultInvalidArgument); - return; - } - const u64 ring = io + ring_offset; - u32 write = memory.Read32(ring + 4); - const u32 read = memory.Read32(ring + 0x44); - if (write >= capacity || read >= capacity || - tasks.size() / 16 > (read + capacity - write - 1) % capacity) { - Reply(ctx, ResultBufferCountReached); - return; - } - for (size_t offset = 0; offset < tasks.size(); offset += 16) { - u64 header{}; - u32 length{}; - std::memcpy(&header, tasks.data() + offset, 8); - std::memcpy(&length, tasks.data() + offset + 8, 4); - const u32 id = (header >> 16) & 0xffff, slot = (header >> 32) & 0xffff; - Result result = ResultSuccess; - u32 output_bytes{}; - const auto it = instances.find(id); - const u64 slot_offset = data_offset + u64{slot} * (input_stride + u64{output_stride}); - if (it == instances.end() || slot >= layout[17] || length > input_stride || - slot_offset > io_size || - input_stride + u64{output_stride} > io_size - slot_offset) { - result = ResultInvalidArgument; - } else { - auto& state = it->second; - const u32 max_frames = std::min(5760U, output_stride / (2 * state.channels)); - if (!max_frames || length > 0x10000) { - result = ResultInvalidArgument; - } else { - std::vector packet(length); - memory.ReadBlock(io + slot_offset, packet.data(), packet.size()); - std::vector pcm(max_frames * state.channels); - // Hardware Opus packets carry an eight-byte big-endian size/range header. - const u8* data = packet.data(); - u32 count = length; - if (length >= 8) { - const u32 framed = (u32{data[0]} << 24) | (u32{data[1]} << 16) | - (u32{data[2]} << 8) | data[3]; - if (framed == length - 8) { - data += 8; - count -= 8; - } - } - if (tasks[offset + 12]) - opus_decoder_ctl(state.decoder, OPUS_RESET_STATE); - const int frames = opus_decode(state.decoder, data, static_cast(count), - pcm.data(), static_cast(max_frames), 0); - if (frames < 0) - result = ResultLibOpusInvalidPacket; - else { - output_bytes = static_cast(frames) * state.channels * 2; - memory.WriteBlock(io + slot_offset + input_stride, pcm.data(), - output_bytes); - } - } - } - if (result != ResultSuccess) { - LOG_ERROR(Service_Audio, - "hwacod task failed instance={} slot={} input_bytes={} result={:#x}", id, - slot, length, result.raw); - } - std::array completion{}; - std::memcpy(completion.data(), &header, 8); - std::memcpy(completion.data() + 16, &output_bytes, 4); - const u32 raw = result.raw; - std::memcpy(completion.data() + 20, &raw, 4); - memory.WriteBlock(ring + 128 + u64{write} * 64, completion.data(), completion.size()); - write = (write + 1) % capacity; - } - memory.Write32(ring + 4, write); - event->Signal(system.Kernel()); - Reply(ctx, ResultSuccess); - } - HwacodSharedParameter layout; - Kernel::KTransferMemory* transfer; - Core::Memory::Memory& memory; - u64 io, io_size; - KernelHelpers::ServiceContext context; - Kernel::KEvent* event{}; - std::map instances; -}; - -class IHwacodClient final : public ServiceFramework { -public: - explicit IHwacodClient(Core::System& system_) - : ServiceFramework{system_, "IHwacodClient"}, impl{system_} { - static const FunctionInfo functions[] = { - {0, &IHwacodClient::InstanceSize, "GetInstanceSize"}, - {1, &IHwacodClient::TaskSize, "GetTaskBufferSize"}, - {10, &IHwacodClient::OpenManager, "OpenManager"}, - }; - RegisterHandlers(functions); - } - -private: - void InstanceSize(HLERequestContext& ctx) { - HwacodParameter p{}; - const auto bytes = ctx.ReadBuffer(); - if (bytes.size() != sizeof(p)) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultInvalidArgument); - return; - } - std::memcpy(p.data(), bytes.data(), sizeof(p)); - u32 size{}; - Result result = ResultNotSupported; - if (p[0] == 1 && !p[1]) { - AudioCore::OpusDecoder::OpusParametersEx params{}; - params.sample_rate = p[2]; - params.channel_count = p[3]; - params.use_large_frame_size = true; - result = impl.GetWorkBufferSizeEx(params, size); - } - LOG_INFO(Service_Audio, "hwacod instance size kind={} rate={} channels={} size={}", p[0], - p[2], p[3], size); - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(result); - rb.Push(size); - } - void TaskSize(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const u32 count = rp.Pop(); - IPC::ResponseBuilder rb{ctx, 3}; - rb.Push(count > 0 && count < 4096 ? ResultSuccess : ResultInvalidArgument); - rb.Push(count > 0 && count < 4096 ? 128 + (count + 1) * 16 : 0U); - } - void OpenManager(HLERequestContext& ctx) { - IPC::RequestParser rp{ctx}; - const auto layout = rp.PopRaw(); - const u64 transfer_size = rp.Pop(), io = rp.Pop(), io_size = rp.Pop(); - const u32 flags = rp.Pop(); - auto transfer = ctx.GetObjectFromHandle(ctx.GetCopyHandle(0)); - LOG_INFO(Service_Audio, - "hwacod open transfer={:#x} io={:#x} size={:#x} flags={} instances={} tasks={} " - "ring={:#x} input={} output={}", - transfer_size, io, io_size, flags, layout[14], layout[17], layout[11], layout[15], - layout[16]); - if (transfer.IsNull() || transfer_size > transfer->GetSize() || layout[0] != 1 || - !layout[14] || layout[14] > 4096 || !layout[17] || layout[17] >= 4096 || !io || - io_size == 0 || io_size > 0x10000000 || io + io_size < io || layout[12] > io_size) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultInvalidArgument); - return; - } - auto& memory = transfer->GetOwner()->GetMemory(); - for (u64 offset = 0; offset < io_size; offset += 0x1000) { - if (!memory.IsValidVirtualAddress(io + offset)) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultInvalidAddressInfo); - return; - } - } - if (!memory.IsValidVirtualAddress(io + io_size - 1)) { - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultInvalidAddressInfo); - return; - } - auto manager = std::make_shared(system, layout, transfer.GetPointerUnsafe(), - io, io_size); - IPC::ResponseBuilder rb{ctx, 3, 1, 1}; - rb.Push(ResultSuccess); - rb.Push(0); - rb.PushCopyObjects(ctx, manager->CompletionEvent()); - rb.PushIpcInterface(ctx, manager); - } - AudioCore::OpusDecoder::OpusDecoderManager impl; -}; -} // namespace Service::Audio diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index eb85d90c01..e2060c90a7 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -3312,12 +3312,7 @@ class INx2GraphicsSystemSession final : public ServiceFramework size={:#x} align={:#x}", w[3], layout.width, layout.height, layout.block_height_log2, layout.pitch, w[8], layout.size, layout.alignment); diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index 3f719da360..4b9c89d38e 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -177,34 +177,6 @@ std::optional AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core:: std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), patchable_section.data()); } - // Star Fox output pacing for the tested build. The engine's explicit-rate path - // updates its render delta as well; simulation timing and the guest clock stay unchanged. - if (name == "main" && process.GetProgramId() == 0x0400E88025B1A000ULL) { - constexpr u32 Fps = 60; - constexpr size_t Offset = 0xB444ADC; - constexpr std::array Original{0xBD4223E1, 0x1E202028, 0x540000ED}; - constexpr std::array BuildId{ - 0xA7, 0x73, 0x0D, 0x0F, 0x74, 0xD1, 0x68, 0xD9, 0x04, 0x22, - 0x35, 0x4C, 0x83, 0x17, 0xF7, 0x6F, 0xF7, 0x8A, 0xD9, 0x38}; - const auto& text = nso_header.segments[0]; - if (std::equal(BuildId.begin(), BuildId.end(), nso_header.build_id.begin()) && - Offset >= text.location && Offset + sizeof(Original) <= u64{text.location} + text.size && - module_start + Offset + sizeof(Original) <= codeset.memory.size() && - std::memcmp(codeset.memory.data() + module_start + Offset, Original.data(), - sizeof(Original)) == 0) { - constexpr std::array replacement{ - 0x52800019U | (Fps << 5), // mov w25, #60 - 0x1E2E1000, // fmov s0, #1.0 - 0x14000035}; // b main+0xB444BB8 (explicit output-rate path) - std::memcpy(codeset.memory.data() + module_start + Offset, replacement.data(), - sizeof(replacement)); - LOG_INFO(Loader, "Star Fox output interval: {} FPS; guest clock unchanged", Fps); - } else { - LOG_WARNING(Loader, - "Star Fox output patch skipped: unsupported build or instructions"); - } - } - // Mario Kart World selects a 1- or 2-vblank frame mode in its main loop. Force its native // 30 FPS mode, or let it choose dynamically using the whole frame time (CPU + GPU wait). // Match the instruction sequence instead of relying on a title-version-specific address. diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 366de0ca3f..66748f2bcd 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -182,7 +182,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const u32 op = instruction.Bits(0, 12); uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0x896 || op == 0xf60 || op == 0xc11 || op == 0xd08 || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 || op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || (op == 0x988 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0xf63 || op == 0xf66 || op == 0xf69 || op == 0xc0b || op == 0x8a5 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)) || op == 0xc0a || - op == 0xf6c || op == 0x4a4 || (op == 0x83b && instruction.Bits(91, 1)); + (op == 0x83b && instruction.Bits(91, 1)); } const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; }; const auto fail = [&](u32 index, std::string_view reason) { @@ -277,7 +277,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 bool rewritten_uniform = false; bool uniform_guard = false; u32 uniform_output = 7; // ULOP3's UPn output: computed in P(6-n), then merged into R239 - if (opcode == 0x2a4 || opcode == 0x4a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 || + if (opcode == 0x2a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 || opcode == 0x292 || opcode == 0x892 || opcode == 0x291 || opcode == 0x896 || opcode == 0x8a5) { // A guarded ULEA without .HI or a carry out lowers to one guarded ISCADD, which // leaves R253 to hold the guest predicate (Pokemon Pokopia's fullscreen passes). @@ -313,7 +313,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 uniform_output = insn.Bits(81, 3); insn.words[2] = (insn.words[2] & ~(7U << 17)) | ((6 - uniform_output) << 17); } - opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x4a4 ? 0x424 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 + opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810 : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 : opcode == 0x896 ? 0x816 : opcode == 0x8a5 ? 0x825 : 0x210; } @@ -370,8 +370,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } // Pd = LUT(a, b, c) over predicate operands (index | negate << 3): each input becomes // 0 or ~0 (SEL), and LOP3 sets Pd when the result is nonzero. - const auto predicate_lut = [&](u32 lut, u32 a, u32 b, u32 c, u32 target, bool uniform_c = false, - u32 second_target = 7, u32 second_lut = 0) { + const auto predicate_lut = [&](u32 lut, u32 a, u32 b, u32 c, u32 target, bool uniform_c = false) { const u32 inputs[3]{a, b, c}; for (u32 i = 0; i < 3; ++i) { if (i == 2 && uniform_c) { @@ -384,28 +383,14 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 emit(0x5c10000000000000ULL | predicate | 252 | (u64{255} << 8) | (u64{252} << 20) | (u64{1} << 48)); } - if (c & 8) { - lut = ((lut & 0x55) << 1) | ((lut & 0xaa) >> 1); - second_lut = ((second_lut & 0x55) << 1) | ((second_lut & 0xaa) >> 1); - } + if (c & 8) lut = ((lut & 0x55) << 1) | ((lut & 0xaa) >> 1); continue; } emit(0x38a0000000000000ULL | predicate | (250 + i) | (u64{255} << 8) | (u64{0x7ffff} << 20) | (u64{1} << 56) | (u64{inputs[i] ^ 8U} << 39)); } - const auto write_lut = [&](u32 table, u32 destination) { - if (destination == 7) return; - emit(0x5be0000000000000ULL | predicate | 255 | (u64{250} << 8) | (u64{251} << 20) | - (u64{table} << 28) | (u64{3} << 36) | (u64{252} << 39) | (u64{destination} << 48)); - }; - // Both results use the saved inputs. Write an execution-guard predicate last. - if (((predicate >> 16) & 7) == target) { - write_lut(second_lut, second_target); - write_lut(lut, target); - } else { - write_lut(lut, target); - write_lut(second_lut, second_target); - } + emit(0x5be0000000000000ULL | predicate | 255 | (u64{250} << 8) | (u64{251} << 20) | + (u64{lut} << 28) | (u64{3} << 36) | (u64{252} << 39) | (u64{target} << 48)); }; const auto mov_imm = [&](u32 reg, u32 value) { emit(0x0100000000000000ULL | predicate | reg | (u64{15} << 12) | (u64{value} << 20)); @@ -949,16 +934,10 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } case 0x81c: { // PLOP3.LUT; bit 67 makes the third source a uniform predicate. const u32 target = insn.Bits(81, 3); - const u32 second_target = insn.Bits(84, 3); const u32 lut = insn.Bits(64, 3) | (insn.Bits(72, 5) << 3); - if (insn.words[1] || (target != 7 && target == second_target)) + if (target == 7 || insn.Bits(84, 3) != 7 || + insn.Bits(16, 8) || insn.words[1]) return fail(index, "native predicate LUT mode"); - if (target == 7 && second_target == 7) break; - if (second_target != 7) { - predicate_lut(lut, insn.Bits(87, 4), insn.Bits(77, 4), insn.Bits(68, 4), target, - insn.Bits(67, 1) != 0, second_target, insn.Bits(16, 8)); - break; - } if (!insn.Bits(67, 1) && (lut == 0xfe || lut == 0xa8)) { emit(0x5090000000000000ULL | predicate | (u64{target} << 3) | 7 | (u64{insn.Bits(87, 4)} << 12) | (u64{1} << 24) | @@ -1023,15 +1002,14 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 emit(0x5c98000000000000ULL | predicate | target | (u64{source} << 20) | (u64{15} << 39)); break; } - case 0xab9: case 0xabb: { // ULDC, direct or indexed, with the same widths as LDC. + case 0xab9: case 0xabb: { // ULDC U32/U64, direct or indexed. const u32 uniform_dest = insn.Bits(16, 8), index_reg = insn.Bits(24, 8); const u32 width = insn.Bits(73, 3); const u32 target = uniform_register(uniform_dest); const u32 index_target = opcode == 0xabb ? uniform_register(index_reg) : 255U; - const u32 alignment = width < 4 ? 1U << (width / 2) : 4U; - if (target == ~0U || target == 255 || width > 5 || + if (target == ~0U || target == 255 || (width != 4 && width != 5) || (width == 5 && ((uniform_dest & 1) || uniform_register(uniform_dest + 1) != target + 1)) || - bank() >= 18 || insn.Bits(38, 16) % alignment || insn.Bits(62, 2) || + bank() >= 18 || insn.Bits(38, 16) % 4 || insn.Bits(62, 2) || (insn.words[2] & ~0xe00U) != (opcode == 0xab9 ? 0U : 0x08000000U) || index_target == ~0U) return fail(index, "native uniform constant load mode"); @@ -1105,9 +1083,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 (u64{insn.Bits(64, 8)} << 20) | (u64{maxwell} << 28)); break; } - case 0xb6c: case 0xf6c: { // TXD 2D/ARRAY_2D; 0xf6c has a uniform texture handle. - const bool bindless = opcode == 0xf6c; - const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; + case 0xb6c: { // TXD 2D or ARRAY_2D, any channels; the two destination pairs anywhere. const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 coordinates = insn.Bits(24, 8), derivatives = insn.Bits(32, 8); // Ampere dimensions: 1 = 2D, 5 = ARRAY_2D; Maxwell TXD's are 2 and 3. The coordinates @@ -1118,33 +1094,23 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // the channel mask: the first two enabled channels go to Rd, Rd+1, the others to the // second destination (RZ drops them; a third channel alone may be in an odd register). const u32 mask = insn.Bits(72, 4), channels = static_cast(std::popcount(mask)); - if ((insn.words[2] & ~(0xfffU | (1U << 26))) != (bindless ? 0x081e0000U : 0x001e0000U) || mask == 0 || insn.Bits(59, 2) || - (dimension != 1 && dimension != 5) || - (bindless ? (handle >= 248 || insn.Bits(46, 13) || derivatives > 244 || coordinates > 245) - : (binding >= 18 || offset > 0x1fff)) || + if ((insn.words[2] & ~(0xfffU | (1U << 26))) != 0x001e0000 || mask == 0 || insn.Bits(59, 2) || + (dimension != 1 && dimension != 5) || binding >= 18 || offset > 0x1fff || (channels > 1 && (dest & 1)) || dest >= 248 || (channels <= 2 && high != 255) || (high != 255 && ((channels > 3 && (high & 1)) || high >= 248)) || coordinates >= 248 || derivatives >= 247) return fail(index, "native gradient texture mode"); - if (!bindless && result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) + if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native texture descriptor buffer conflict"); - if (!bindless) result.required_texture_buffer = binding; + result.required_texture_buffer = binding; // Maxwell writes the channels to consecutive registers: when the second destination // doesn't follow Rd (Splatoon Raiders: R12/R18, R18/R44), write them to scratch and move // each into place. - const bool consecutive = !bindless && (channels <= 2 || high == dest + 2); + const bool consecutive = channels <= 2 || high == dest + 2; const u64 target = consecutive ? dest : 248; - if (bindless) { - // Maxwell TXD.B expects the handle before x, y and the optional array layer. - emit(0x5c98000000000000ULL | predicate | 248 | (u64{15} << 39) | (u64{handle} << 20)); - for (u32 i = 0; i < (dimension == 5 ? 3U : 2U); ++i) { - emit(0x5c98000000000000ULL | predicate | (249 + i) | (u64{15} << 39) | - (u64{coordinates + i} << 20)); - } - } - emit((bindless ? 0xde40000000000000ULL : 0xde00000000000000ULL) | predicate | target | (u64{bindless ? 248U : coordinates} << 8) | + emit(0xde00000000000000ULL | predicate | target | (u64{coordinates} << 8) | (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{mask} << 31) | - (bindless ? 0ULL : u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); + (u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); if (!consecutive) { for (u32 i = 0; i < channels; ++i) { if (i >= 2 && high == 255) break; @@ -1361,31 +1327,19 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); break; } - case 0x593: // SUATOM.D.2D/3D.ADD/OR.STRONG.GPU.IGN U32, returning the old value. - case 0x59f: { // SURED.D.2D/3D.ADD/OR.STRONG.GPU.IGN U32, direct descriptor. + case 0x59f: { // SURED.D.2D.ADD.STRONG.GPU.IGN U32, direct descriptor (title 1's race compute). const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14); const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8); - const u32 dimension = insn.Bits(61, 3); - const bool returns_value = opcode == 0x593; - const u32 operation = insn.Bits(87, 3); // ADD = 0, OR = 6 on both architectures. - const u32 mode = returns_value ? 0x001ee000U : 0x0010e000U; - if ((insn.words[2] & ~0x03800000U) != mode || (operation != 0 && operation != 6) || - (dimension != 3 && dimension != 5) || binding >= 18 || - offset > 0x1fff || insn.Bits(59, 2) || coordinate > (dimension == 5 ? 252U : 253U)) + if (insn.words[2] != 0x0010e000 || insn.Bits(61, 3) != 3 || binding >= 18 || + offset > 0x1fff || insn.Bits(59, 2) || coordinate > 253) return fail(index, "native surface reduction mode"); if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding) return fail(index, "native surface descriptor buffer conflict"); result.required_texture_buffer = binding; - if (returns_value) { - emit(base(0xea00000000000000ULL) | (u64{coordinate} << 8) | - (u64{data} << 20) | (u64{operation} << 29) | (u64{dimension} << 33) | - (u64{offset} << 36)); - break; - } - // Maxwell SURED: operand 0-7, coordinates 8-15, operation 24, U32 = 0 at 20, - // type 33 (3 = 2D, 5 = 3D), bound offset 36, bound 51. + // Maxwell SURED (as the frontend decodes it): operand 0-7, coordinates 8-15, ADD and + // U32 are zero at 24 and 20, type 33 (3 = 2D), bound offset 36, bound 51. emit(0xeb40000000000000ULL | predicate | data | (u64{coordinate} << 8) | - (u64{operation} << 24) | (u64{dimension} << 33) | (u64{offset} << 36) | (u64{1} << 51)); + (u64{3} << 33) | (u64{offset} << 36) | (u64{1} << 51)); break; } case 0x301: // BREV register. @@ -1401,14 +1355,6 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 const bool shift_operand = constant ? valid_cbuf() && !insn.Bits(62, 2) : insn.words[1] <= 255; const auto load_shift = [&] { if (constant) materialize(2, 0, 251, false, false); }; const bool right = insn.Bits(76, 1) != 0, high_result = insn.Bits(80, 1) != 0; - if (right && high_result && low == 255 && insn.Bits(73, 2) == 2 && - shift_operand && shift != ~0U && - (insn.words[2] & ~(0x000118ffU | (opcode == 0xc19 ? 0x08000000U : 0U))) == 0x00000400U) { - load_shift(); - emit(base(0x5c28000000000000ULL) | (u64{high} << 8) | (u64{shift} << 20) | - (u64{1} << 48) | (u64{insn.Bits(75, 1)} << 39)); - break; - } // SHF.R.U32 of the low word with a zero high word: a plain logical right shift. if (right && !high_result && high == 255 && insn.Bits(73, 2) == 3 && shift != ~0U && shift_operand && @@ -2212,21 +2158,18 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 } break; } - case 0xb63: case 0xf63: { // TLD4 (gather), 2D/ARRAY_2D; component at bits 87-88. + case 0xb63: case 0xf63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88. // 0xf63: the handle in a uniform register (bit 91), Maxwell TLD4.B with the handle in Rb. const bool bindless = opcode == 0xf63; const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255; const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8); const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8); const u32 binding = insn.Bits(54, 5); - const bool array = insn.Bits(61, 3) == 5; // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb. const bool offsets = insn.Bits(76, 1) != 0; - if ((!array && insn.Bits(61, 3) != 1) || (array && offsets) || - ((array || offsets) && source_a != 255 && source_a + 1 >= shadow_boundary) || - insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || - (offsets && (bindless || source_b == 255)) || + if (insn.Bits(61, 3) != 1 || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) || + (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) || insn.Bits(89, 1) || mask == 0 || (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1)) : (insn.Bits(40, 14) > 0x1fff || binding >= 18 || @@ -2237,24 +2180,18 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // with an offset, x and y in Ra, Ra+1 and the offset in Rb. const u32 y = !offsets ? source_b : source_a == 255 ? 255U : source_a + 1; emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20)); - if (array) { - const u32 x = source_a == 255 ? 255U : source_a + 1; - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{x} << 20)); - emit(0x5c98000000000000ULL | predicate | 252 | (u64{15} << 39) | (u64{y} << 20)); - } else { - emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); - } + emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20)); if (offsets) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20)); } if (bindless) { emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20)); emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) | - (u64{array ? 3U : 2U} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | + (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51)); } else { emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | - (u64{offsets ? 246U : 255U} << 20) | (u64{array ? 3U : 2U} << 28) | (u64{mask} << 31) | + (u64{offsets ? 246U : 255U} << 20) | (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56)); } @@ -2697,9 +2634,7 @@ inline LoweredProgram LowerToMaxwellWith(std::span code, u32 // F32), 84-85 the source size (0: 8, 1: 16 bits). const u32 source = opcode == 0xd06 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8); const u64 is_signed = insn.Bits(74, 1); - const bool uniform = opcode == 0xd06; - if ((insn.words[2] & ~0x400U) == (uniform ? 0x08101000U : 0x00101000U) && - !(insn.words[1] & 0xefffff00U) && source != ~0U) { + if (opcode == 0x306 && (insn.words[2] & ~0x400U) == 0x00101000 && !(insn.words[1] & 0xcfffff00U)) { // I2F.{U,S}16 Rd, Rs(.H1): Maxwell's 16-bit source format, byte selector 0 or 2. emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{1} << 10) | (is_signed << 13) | (u64{source} << 20) | (u64{insn.Bits(60, 2) * 2} << 41)); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a617143dca..998a62821a 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -32,7 +32,6 @@ add_executable(tests video_core/ampere_lowering.cpp video_core/ampere_geometry.cpp video_core/ampere_i2f.cpp - video_core/ampere_gradient.cpp video_core/ampere_returns.cpp video_core/ampere_guarded_ulea.cpp video_core/ampere_compute_lowering.cpp diff --git a/src/tests/video_core/ampere_gradient.cpp b/src/tests/video_core/ampere_gradient.cpp deleted file mode 100644 index 42570c8fbc..0000000000 --- a/src/tests/video_core/ampere_gradient.cpp +++ /dev/null @@ -1,221 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#include -#include -#include -#include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" - -TEST_CASE("Ampere surface reductions preserve 3D coordinates", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - using Shader::Ampere::LowerToMaxwell; - const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; - // Star Fox: atomic unsigned add of R3 to the 3D image at c[0][0x88], xyz in R0-R2. - Instruction sured{{0x0000759f, 0xa0002203, 0x0010e000, 0x0011e600}}; - for (const u32 dimension : {3U, 5U}) { - sured.words[1] = (sured.words[1] & 0x1fffffffU) | (dimension << 29); - const auto lowered = LowerToMaxwell(std::array{sured, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(lowered.required_texture_buffer == 0); - REQUIRE(lowered.instructions.size() == 2); - const u64 atomic = lowered.instructions[0]; - REQUIRE((atomic & 0xfff0000000000000ULL) == 0xeb40000000000000ULL); - REQUIRE((atomic & 0xff) == 3); // data register - REQUIRE(((atomic >> 8) & 0xff) == 0); // first coordinate - REQUIRE(((atomic >> 16) & 15) == 7); // original guard - REQUIRE(((atomic >> 20) & 7) == 0); // U32 - REQUIRE(((atomic >> 24) & 7) == 0); // ADD - REQUIRE(((atomic >> 33) & 7) == dimension); - REQUIRE(((atomic >> 36) & 0x1fff) == 0x22); // descriptor word offset - REQUIRE(((atomic >> 51) & 1) == 1); // bound descriptor - } - sured.words[0] = 0xfd00759f; // R253-R255 would cross into the zero register in 3D - REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{sured, exit}))); - sured.words[0] = 0x0a00859f; // xyz in R10-R12, guarded by !P0 - auto lowered = LowerToMaxwell(std::array{sured, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(((lowered.instructions[0] >> 8) & 255) == 10); - REQUIRE(((lowered.instructions[0] >> 16) & 15) == 8); - sured = {{0x0000759f, 0xa0002403, 0x0310e000, 0x000fe200}}; - lowered = LowerToMaxwell(std::array{sured, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(((lowered.instructions[0] >> 24) & 7) == 6); // OR - REQUIRE(((lowered.instructions[0] >> 33) & 7) == 5); // still 3D - sured.words[2] = 0x0010e000; - sured.words[2] |= 1U << 23; // unimplemented atomic operation must still be rejected - REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{sured, exit}))); -} - -TEST_CASE("Ampere surface atomic add returns the old 3D texel value", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - using Shader::Ampere::LowerToMaxwell; - const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; - Instruction atomic{{0x00087593, 0xa0002407, 0x001ee000, 0x000ea400}}; - const auto lowered = LowerToMaxwell(std::array{atomic, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(lowered.required_texture_buffer == 0); - REQUIRE(lowered.instructions.size() == 2); - const u64 word = lowered.instructions[0]; - REQUIRE((word & 0xff80000000000000ULL) == 0xea00000000000000ULL); - REQUIRE((word & 255) == 8); // old texel value returned to R8 - REQUIRE(((word >> 8) & 255) == 0); // xyz R0-R2 - REQUIRE(((word >> 20) & 255) == 7); // addend R7 - REQUIRE(((word >> 29) & 15) == 0); // ADD - REQUIRE(((word >> 33) & 7) == 5); // 3D - REQUIRE(((word >> 36) & 0x1fff) == 0x24); // c[0][0x90] - REQUIRE(((word >> 51) & 7) == 0); // U32 - REQUIRE(((word >> 54) & 1) == 0); // bound descriptor - atomic.words[2] &= ~(7U << 17); // writing a fault predicate is not yet supported - REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{atomic, exit}))); -} - -TEST_CASE("Ampere gradient textures accept uniform array handles", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - using Shader::Ampere::LowerToMaxwell; - const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; - // Star Fox's material shader: uniform handle UR4, coordinates R20, derivatives R16. - Instruction txd{{0x14127f6c, 0xa0000410, 0x081e0f10, 0x001f4200}}; - auto lowered = LowerToMaxwell(std::array{txd, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(lowered.required_texture_buffer == ~0U); - REQUIRE(lowered.instructions.size() == 10); - REQUIRE(lowered.instructions[0] == 0x5c9807800dc700f8ULL); - const u64 texture = lowered.instructions[4]; - REQUIRE((texture & 0xffc0000000000000ULL) == 0xde40000000000000ULL); - REQUIRE(((texture >> 8) & 0xff) == 248); - REQUIRE(((texture >> 20) & 0xff) == 16); - REQUIRE(((texture >> 28) & 7) == 3); - REQUIRE(((texture >> 31) & 15) == 15); - REQUIRE((lowered.instructions[5] & 0xff) == 18); - REQUIRE((lowered.instructions[7] & 0xff) == 16); - - txd = {{0x18320f6c, 0xa000041c, 0x0c1e0f34, 0x00414200}}; - lowered = LowerToMaxwell(std::array{txd, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(((lowered.instructions[4] >> 49) & 1) == 1); - REQUIRE(((lowered.instructions[4] >> 16) & 15) == 0); - - txd.words[1] = 0xa0004020; // invalid uniform handle - REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{txd, exit}))); -} - -TEST_CASE("Ampere array gathers preserve the layer and split coordinates", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - const std::array code{{ - {{0x201c7b63, 0xb0000a2b, 0x041e0f16, 0x000f4200}}, - {{0x0000794d, 0, 0x03800000, 0}}, - }}; - const auto lowered = Shader::Ampere::LowerToMaxwell(code); - REQUIRE(static_cast(lowered)); - REQUIRE(((lowered.instructions[0] >> 20) & 0xff) == 32); - REQUIRE(((lowered.instructions[1] >> 20) & 0xff) == 33); - REQUIRE(((lowered.instructions[2] >> 20) & 0xff) == 43); - REQUIRE(((lowered.instructions[3] >> 28) & 7) == 3); -} - -TEST_CASE("Ampere gathers keep coordinate pairs below reserved registers", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - struct Mode { - Instruction instruction; - u32 boundary; - u32 high_uniforms; - bool pair; - }; - const std::array modes{ - Mode{{{0x201c7b63, 0xb0000a2b, 0x041e0f16, 0x000f4200}}, 244, 0, true}, - Mode{{{0xd61c7f63, 0xb000002b, 0x0c1e0f16, 0x000f4200}}, 216, 0, true}, - Mode{{{0xd61c7f63, 0xb000002b, 0x0c1e0f16, 0x000f4200}}, 212, 4, true}, - Mode{{{0x201c7b63, 0x30000a2b, 0x041e1f16, 0x000f4200}}, 244, 0, true}, - Mode{{{0x201c7b63, 0x30000a2b, 0x041e0f16, 0x000f4200}}, 244, 0, false}, - Mode{{{0xd61c7f63, 0x3000002b, 0x0c1e0f16, 0x000f4200}}, 216, 0, false}, - }; - for (const auto& mode : modes) { - for (const u32 source : {mode.boundary - 2, mode.boundary - 1, 255U}) { - CAPTURE(mode.boundary, mode.high_uniforms, mode.pair, source); - auto gather = mode.instruction; - gather.words[0] = (gather.words[0] & 0x00ffffffU) | (source << 24); - const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; - const auto lowered = Shader::Ampere::LowerToMaxwellWith( - std::array{gather, exit}, mode.high_uniforms); - if (mode.pair && source == mode.boundary - 1) { - REQUIRE_FALSE(static_cast(lowered)); - REQUIRE(lowered.error == "native texture gather mode"); - REQUIRE(lowered.instructions.empty()); - continue; - } - REQUIRE(static_cast(lowered)); - REQUIRE(lowered.instructions.size() >= 3); - REQUIRE(((lowered.instructions[0] >> 20) & 255) == source); - if (mode.pair) { - REQUIRE(((lowered.instructions[1] >> 20) & 255) == - (source == 255 ? 255U : source + 1)); - REQUIRE(((lowered.instructions[2] >> 20) & 255) == 43); - } else { - REQUIRE(((lowered.instructions[1] >> 20) & 255) == 43); - } - } - } -} - -TEST_CASE("Ampere signed high-word shifts keep sign extension", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - const std::array code{{ - {{0xff247219, 0x00000025, 0x00011444, 0x000fc600}}, - {{0x0000794d, 0, 0x03800000, 0}}, - }}; - const auto lowered = Shader::Ampere::LowerToMaxwell(code); - REQUIRE(static_cast(lowered)); - REQUIRE(((lowered.instructions[0] >> 8) & 0xff) == 68); - REQUIRE(((lowered.instructions[0] >> 20) & 0xff) == 37); - REQUIRE(((lowered.instructions[0] >> 48) & 1) == 1); -} - -TEST_CASE("Ampere uniform multiply accepts an immediate addend", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - const std::array code{{ - {{0x040474a4, 0x00000001, 0x0f8e0205, 0x000fe200}}, - {{0x0000794d, 0, 0x03800000, 0}}, - }}; - const auto lowered = Shader::Ampere::LowerToMaxwell(code); - REQUIRE(static_cast(lowered)); - REQUIRE((lowered.instructions[lowered.instructions.size() - 2] & 255) == 220); -} - -TEST_CASE("Ampere dual predicate LUTs read original inputs and preserve their guard", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - for (const u32 guard : {0U, 7U, 8U}) { - const std::array code{{ - {{0x00c8081c | (guard << 12), 0, 0x0010f010, 0x000fc400}}, - {{0x0000794d, 0, 0x03800000, 0}}, - }}; - const auto lowered = Shader::Ampere::LowerToMaxwell(code); - REQUIRE(static_cast(lowered)); - for (u32 input = 0; input < 4; ++input) { - std::array predicates{}; - predicates[0] = input & 1; - predicates[1] = input & 2; - predicates[7] = true; - const auto before = predicates; - std::array registers{}; - const auto pred = [&](u32 encoded) { return predicates[encoded & 7] != bool(encoded & 8); }; - for (const u64 word : lowered.instructions) { - if ((word >> 56) == 0xe3) continue; // EXIT - if (!pred((word >> 16) & 15)) continue; - if ((word >> 52) == 0x39a) { - // SEL Rd, RZ, -1, P: select zero when P is true. - registers[word & 255] = !pred((word >> 39) & 15); - } else { - REQUIRE((word >> 52) == 0x5be); - const u32 index = (registers[(word >> 8) & 255] ? 4U : 0U) | - (registers[(word >> 20) & 255] ? 2U : 0U) | - (registers[(word >> 39) & 255] ? 1U : 0U); - const bool value = (word >> (28 + index)) & 1; - const u32 destination = (word >> 48) & 7; - if (destination != 7) predicates[destination] = value; - } - } - const bool active = before[guard & 7] != bool(guard & 8); - const u32 index = (before[0] ? 4U : 0U) | 2U | (before[1] ? 1U : 0U); - REQUIRE(predicates[0] == (active ? bool((0x80 >> index) & 1) : before[0])); - REQUIRE(predicates[1] == (active ? bool((0xc8 >> index) & 1) : before[1])); - } - } -} diff --git a/src/tests/video_core/ampere_i2f.cpp b/src/tests/video_core/ampere_i2f.cpp index d793650091..eed0388085 100644 --- a/src/tests/video_core/ampere_i2f.cpp +++ b/src/tests/video_core/ampere_i2f.cpp @@ -42,33 +42,4 @@ TEST_CASE("Ampere I2F keeps unsigned 16-bit sources unsigned", "[video_core][amp REQUIRE(Contains(lowered, 0x5cb8000000170601ULL)); } -TEST_CASE("Ampere I2F converts 16-bit uniform sources", "[video_core][ampere]") { - // Star Fox's graphics shader: I2F.F32.U16 R2, UR4. - std::array code{{ - {{0x00027d06, 0x00000004, 0x08101000, 0x000e6200}}, - EXIT, - }}; - const auto lowered = LowerToMaxwell(code); - REQUIRE(static_cast(lowered)); - // UR4 is mirrored in R220; preserve unsigned 16-bit conversion. - REQUIRE(Contains(lowered, 0x5cb800000dc70602ULL)); - - code[0] = {{0x00047d06, 0x10000004, 0x08101000, 0x000ef000}}; - const auto upper_half = LowerToMaxwell(code); - REQUIRE(static_cast(upper_half)); - REQUIRE(Contains(upper_half, 0x5cb804000dc70604ULL)); - code[0].words[1] = 0x20000004; - REQUIRE_FALSE(static_cast(LowerToMaxwell(code))); - - code[0] = {{0x00027d06, 0x00000004, 0x08101000, 0x000e6200}}; - - code[0].words[2] |= 0x400; - const auto signed_lowered = LowerToMaxwell(code); - REQUIRE(static_cast(signed_lowered)); - REQUIRE(Contains(signed_lowered, 0x5cb800000dc72602ULL)); - - code[0].words[1] = 64; - REQUIRE_FALSE(static_cast(LowerToMaxwell(code))); -} - } // namespace diff --git a/src/tests/video_core/ampere_ldc.cpp b/src/tests/video_core/ampere_ldc.cpp index 6c42bf7961..6c365ec578 100644 --- a/src/tests/video_core/ampere_ldc.cpp +++ b/src/tests/video_core/ampere_ldc.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include #include @@ -39,26 +38,3 @@ TEST_CASE("Ampere indexed constant loads retain narrow sizes", "[video_core][amp unsupported.words[2] = 1U << 14; REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{unsupported, exit}))); } - -TEST_CASE("Ampere uniform constant loads retain narrow sizes", "[video_core][ampere]") { - using Shader::Ampere::Instruction; - using Shader::Ampere::LowerToMaxwell; - const Instruction exit{{0x0000794d, 0, 0x03800000, 0}}; - // Star Fox: ULDC.U16 UR4, c[6][0x20]. - Instruction load{{0x00047ab9, 0x01800800, 0x00000400, 0x000fe200}}; - auto lowered = LowerToMaxwell(std::array{load, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(std::find(lowered.instructions.begin(), lowered.instructions.end(), - 0xef9200600207ffdcULL) != lowered.instructions.end()); - for (u32 width : {0U, 1U, 2U, 3U, 5U}) { - load.words[2] = width << 9; - lowered = LowerToMaxwell(std::array{load, exit}); - REQUIRE(static_cast(lowered)); - REQUIRE(((lowered.instructions[0] >> 48) & 7) == width); - } - load.words[2] = 6U << 9; - REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{load, exit}))); - load.words[2] = 2U << 9; - load.words[1] |= 1U << 6; - REQUIRE_FALSE(static_cast(LowerToMaxwell(std::array{load, exit}))); -} diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 49ebd75005..23ffbad790 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -894,8 +894,7 @@ bool KeplerCompute::RememberDlssMotionVectors() { // the input colour through cbuf0+0x328 and writes the DLSS output through cbuf0+0x348 (B10G11R11; // 720p to 1440p docked). Blit one to the other with bilinear filtering when both are plain 2D // images of the same format and the output is larger: title 4's slots first, then Fortune's -// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. Star Fox's -// known 1600x900 RGBA16F -> 1920x1080 RGB10A2 pair also needs format conversion. +// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. bool KeplerCompute::UpscaleInsteadOfNativeProgram() { const auto& cbuf = launch_description.const_buffer_config[0]; const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low; @@ -905,27 +904,19 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { return GetTICEntry(tic_index); }; const auto upscale_pair = [](const std::optional& from, - const std::optional& to, - bool allow_conversion = false) { - return from && to && (from->format == to->format || allow_conversion) && from->Address() != 0 && + const std::optional& to) { + return from && to && from->format == to->format && from->Address() != 0 && to->Address() != 0 && to->Width() > from->Width() && from->IsBlockLinear() && to->IsBlockLinear(); }; std::optional input = image(0x90), output = image(0xa0); - const bool starfox_conversion = Settings::GetCurrentProgramID() == 0x0400E88025B1A000ULL && - input && output && input->format == Texture::TextureFormat::R16G16B16A16 && - output->format == Texture::TextureFormat::A2B10G10R10 && - input->Width() == 1600 && input->Height() == 900 && - output->Width() == 1920 && output->Height() == 1080; - bool have_pair = upscale_pair(input, output, starfox_conversion); bool fortunes_weave = false; - if (!have_pair) { + if (!upscale_pair(input, output)) { input = image(0x328); output = image(0x348); fortunes_weave = upscale_pair(input, output); - have_pair = fortunes_weave; } - if (!have_pair) { + if (!upscale_pair(input, output)) { input.reset(); output.reset(); for (u32 first = 0x80; first < 0x120 && !input; first += 4) { @@ -945,21 +936,15 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { if (!input || !output) return false; using RTFormat = Tegra::RenderTargetFormat; - const auto render_format = [](Texture::TextureFormat format) -> std::optional { - switch (format) { - case Texture::TextureFormat::A2B10G10R10: return RTFormat::A2B10G10R10_UNORM; - case Texture::TextureFormat::A8B8G8R8: return RTFormat::A8B8G8R8_UNORM; - case Texture::TextureFormat::B10G11R11: return RTFormat::B10G11R11_FLOAT; - case Texture::TextureFormat::R16G16B16A16: return RTFormat::R16G16B16A16_FLOAT; - default: return std::nullopt; - } - }; - const auto input_format = render_format(input->format); - const auto output_format = render_format(output->format); - if (!input_format || !output_format) { - return false; - } - const auto surface = [&](const Texture::TICEntry& tic, RTFormat format) { + RTFormat format{}; + switch (input->format) { + case Texture::TextureFormat::A2B10G10R10: format = RTFormat::A2B10G10R10_UNORM; break; + case Texture::TextureFormat::A8B8G8R8: format = RTFormat::A8B8G8R8_UNORM; break; + case Texture::TextureFormat::B10G11R11: format = RTFormat::B10G11R11_FLOAT; break; + case Texture::TextureFormat::R16G16B16A16: format = RTFormat::R16G16B16A16_FLOAT; break; + default: return false; + } + const auto surface = [&](const Texture::TICEntry& tic) { Fermi2D::Surface result{}; result.format = format; result.linear = Fermi2D::MemoryLayout::BlockLinear; @@ -1056,7 +1041,7 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { (void)fake; } } - auto source = surface(*input, *input_format); + auto source = surface(*input); // EDEN_NX2_UPSCALE_SOURCE=: blit from another same-sized image (debug aid). if (static const u64 override_source = [] { const char* value = std::getenv("EDEN_NX2_UPSCALE_SOURCE"); @@ -1080,13 +1065,14 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { if (!fortunes_weave || !enabled || !dlss_motion_vectors || dlss_motion_color != input->Address()) return false; - Fermi2D::Surface motion = surface(*dlss_motion_vectors, RTFormat::R16G16_FLOAT); - return rasterizer->AccelerateTemporalUpscale(source, surface(*output, *output_format), motion, + Fermi2D::Surface motion = surface(*dlss_motion_vectors); + motion.format = RTFormat::R16G16_FLOAT; + return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, dlss_depth ? &*dlss_depth : nullptr, dlss_jitter, {1.0f, 1.0f}); }; const bool done = upscale_temporally() || - rasterizer->AccelerateSurfaceCopy(source, surface(*output, *output_format), config); + rasterizer->AccelerateSurfaceCopy(source, surface(*output), config); if (static u32 checked{}; checked++ == sample_at) { const u64 size = u64{output->Width()} * output->Height() * 4; if (const auto device = memory_manager.GpuToCpuAddress(output->Address())) {