Compare commits

...

9 Commits

Author SHA1 Message Date
Emil 313b60e922 Capture PCM at set time when multiple AudioOut2 ports share a buffer (#826)
* capture PCM at set time when ports share a buffer

* Snapshot size limit and open device check

* Fall back to PCM pointer on stale snapshot

* audio: own AudioOut2 PCM at attribute set time

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 02:19:49 +04:00
Hamza Shakir cda236dbb3 Shader recompiler: accept V_NOT_B32 SDWA partial dst with any source; allow 64 buffers
SDWA source extraction and destination insertion are independent. Cover the reported word1-to-word0 preserve form and byte-source partial destinations in compute tests. Verify 64 distinct compute buffers survive resource tracking and binding layout, and remove the obsolete CFG rejection.

(cherry picked from commit 9e74df7dc0)
2026-09-26 01:58:55 +04:00
Henrique Reis Neves 1216fa36cf docs: fix stale README notes, document building without Qt (#812)
* docs: fix stale README notes, document building without Qt

- Use the current issue template name, Game Emulation Status Report
  (renamed in 027433f).
- Raise the documented Windows CMake minimum from 3.12 to 3.22.1, which
  the bundled SPIRV-Tools requires.
- Note that -DKYTY_BUILD_LAUNCHER=OFF builds kyty_emulator and the
  regression tests without Qt (#193).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: add Qt-free Linux build commands

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 01:50:06 +04:00
nmzik 497166009c loader: unlink exact module imports before releasing their mapping 2026-09-25 21:15:41 +04:00
nmzik 0fb3ac5605 kernel: retain modules until their final explicit unload 2026-09-25 21:15:41 +04:00
nmzik e056d55dd5 loader: consolidate eager symbol resolution and remove dead PLT machinery 2026-09-25 21:15:41 +04:00
nmzik 1371219f5c loader: preserve resolved relocations across dynamic loads 2026-09-25 21:15:41 +04:00
nmzik cf423df624 shader: remove per-draw vertex diagnostic atomics 2026-09-25 21:15:41 +04:00
nmzik 5ce4f08364 audio: improve NGS2 stream and ATRAC9 playback 2026-09-25 18:26:08 +04:00
17 changed files with 906 additions and 590 deletions
+15 -3
View File
@@ -79,7 +79,7 @@ or graphical glitches, so please include the version you tested when reporting a
## Contributing
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
issues first, then use the **Game Emulation Bug Report** template and attach the complete log file.
issues first, then use the **Game Emulation Status Report** template and attach the complete log file.
Code contributions should be focused, build successfully on the platforms they touch, and include
relevant tests where practical. Windows is the primary target, so a change that alters shared code
@@ -132,7 +132,7 @@ the Vulkan/SPIR-V validation rules.
### Build requirements (Windows)
- Git
- CMake 3.12 or newer
- CMake 3.22.1 or newer
- Ninja
- Visual Studio 2022 or Build Tools 2022 with the **Desktop development with C++** workload and
**C++ Clang tools for Windows** component
@@ -178,7 +178,7 @@ sudo apt-get install --no-install-recommends \
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
```
Qt 6 (Concurrent, Network, Widgets) is also required — either the distribution packages
Qt 6 (Concurrent, Network, Widgets) is required for the launcher — either the distribution packages
(`qt6-base-dev`) or an official Qt installation.
```bash
@@ -197,6 +197,18 @@ The install step copies the Qt libraries and plugins next to the binaries, so
from the pinned [KytyPS5 FFmpeg core](https://github.com/KytyPS5/ext-ffmpeg-core)
release, including VP9 and WebM support. System FFmpeg packages are not required.
To build `kyty_emulator` and the `kyty_tests` target without Qt, use a separate build directory:
```bash
git submodule update --init --recursive
cmake -S . -B _Build/linux-no-qt -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DKYTY_BUILD_LAUNCHER=OFF
cmake --build _Build/linux-no-qt --target kyty_emulator kyty_tests --parallel
```
As on Windows, the MSVC compiler is not used; Clang is required. `cl.exe` is rejected at configure
time.
@@ -522,8 +522,10 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
// The source field extract and the destination insert are independent (Translate.cpp), so a
// partial destination may combine with any source selector.
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelFull(), false},
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true},
{Opcode::V_CVT_F16_F32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
+1 -1
View File
@@ -435,7 +435,7 @@ struct BindingLayout {
};
struct ShaderInfo {
static constexpr uint32_t MaxBuffers = 32;
static constexpr uint32_t MaxBuffers = 64;
static constexpr uint32_t MaxImages = 64;
static constexpr uint32_t MaxSamplers = 32;
static constexpr uint32_t MaxSampledPairs = 64;
-18
View File
@@ -409,16 +409,6 @@ static void ShaderApplyAttribSemantics(ShaderVertexInputInfo& info,
uint32_t offset = (attrib[in.semantic] >> 14u) & 0xfffu;
uint32_t fetch_index = (attrib[in.semantic] >> 26u) & 0x1u;
if (fetch_index != 0) {
static std::atomic<uint64_t> log_count = 0;
auto log_id = log_count.fetch_add(1);
if (log_id < 64) {
LOGF("\t temporary: PS5 vertex attrib semantic %u uses fetch index %u, buffer "
"index %zu\n",
static_cast<uint32_t>(in.semantic), fetch_index, index);
}
}
EXIT_NOT_IMPLEMENTED(index >= ShaderVertexInputInfo::RES_MAX);
const auto* sharp = &buffer[index * 4];
@@ -439,14 +429,6 @@ static void ShaderApplyAttribSemantics(ShaderVertexInputInfo& info,
const auto format_raw = static_cast<uint32_t>(format);
const auto buffer_format = format_raw >> 2u;
const auto channels = (format_raw & 3u) + 1u;
static std::atomic<uint64_t> log_count = 0;
auto log_id = log_count.fetch_add(1);
if (log_id < 64) {
LOGF("\t PS5 vertex attrib semantic %u uses attrib format %u -> buffer "
"format %u, offset %u, buffer index %zu\n",
static_cast<uint32_t>(in.semantic), static_cast<uint32_t>(format),
static_cast<uint32_t>(buffer_format), offset, index);
}
// AGC vertex formats encode the buffer format above the two channel-count bits.
// The fetch prolog selects X001, XY01, XYZ1, or XYZW from that count.
r.fields[3] = (r.fields[3] & ~((0x7fu << 12u) | 0xfffu)) |
+22 -12
View File
@@ -188,7 +188,7 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
const void* pcm_data = nullptr;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
@@ -341,11 +341,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr;
}
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
return static_cast<size_t>(state.samples_num) *
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
}
}
@@ -353,24 +359,23 @@ static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
// The backend consumes the buffers synchronously, but can block while pacing SDL's queue.
// Keep both PCM storage and port handles alive until it returns.
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
g_audioout2_port_mutex.Lock();
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data});
!state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
}
}
g_audioout2_port_mutex.Unlock();
if (params.empty()) {
return;
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
static void audioout2_close_audio_handle(int audio_handle) {
@@ -679,7 +684,12 @@ int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
state->pcm_data = pcm_data;
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
}
g_audioout2_port_mutex.Unlock();
}
+7
View File
@@ -1226,6 +1226,7 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
auto* program = rt->FindProgramByFileName(module_path);
if (program != nullptr) {
++program->load_count;
if (res != nullptr) {
*res = OK;
}
@@ -1240,6 +1241,7 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
rt->RelocateProgram(program);
program->load_count = 1;
int result = rt->StartModule(program, args, argp, nullptr);
LOGF("\tmodule_start() result = %d\n", result);
@@ -1269,6 +1271,11 @@ static int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args
if (program == nullptr) {
LOGF("\tinvalid module handle = %" PRId32 "\n", handle);
return KERNEL_ERROR_ESRCH;
}
if (program->load_count > 1) {
--program->load_count;
return OK;
}
+342 -104
View File
@@ -11,6 +11,7 @@
#include <array>
#include <atomic>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <deque>
#include <limits>
@@ -197,8 +198,8 @@ struct Ngs2WaveformFormat {
uint32_t num_channels = 0;
uint32_t sample_rate = 0;
uint32_t config_data = 0;
uint32_t frame_margin = 0;
uint32_t frame_offset = 0;
uint32_t frame_margin = 0;
};
struct Ngs2WaveformBlock {
@@ -388,18 +389,20 @@ struct Ngs2RackInternal {
enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb };
// The 64-bit bypass mask precedes the filter type and FCQ parameters.
struct Ngs2SamplerFilterParam {
uint32_t index;
uint32_t location;
uint64_t channel_mask;
uint32_t type;
uint64_t channel_mask;
float frequency;
float q;
float level;
uint32_t reserved[3];
};
static_assert(sizeof(Ngs2SamplerFilterParam) == 48);
static_assert(offsetof(Ngs2SamplerFilterParam, type) == 8 &&
offsetof(Ngs2SamplerFilterParam, channel_mask) == 16 &&
offsetof(Ngs2SamplerFilterParam, frequency) == 24);
struct Ngs2SamplerFilter {
struct History {
@@ -507,7 +510,6 @@ struct Ngs2VoiceInternal {
Ngs2WaveformBlock info;
uint32_t cursor = 0;
size_t data_cursor = 0;
uint32_t skip_remaining = info.num_skip_samples;
uint32_t repeated_count = 0;
};
struct Module {
@@ -535,14 +537,21 @@ struct Ngs2VoiceInternal {
bool has_samples = false;
std::unique_ptr<Ajm::AjmAt9Decoder> decoder;
std::vector<float> decoded_frame;
bool accepts_blocks = true;
uint32_t sample_rate = 0;
uint64_t sample_phase = 0;
uint64_t sample_step = 0;
uint64_t decoded_samples = 0;
uint64_t decoded_bytes = 0;
const uint8_t* waveform_end = nullptr;
std::vector<float> decoded_frame;
std::vector<float> next_decoded_frame;
std::vector<uint8_t> compressed_input;
uint32_t frame_cursor = 0;
uint32_t frame_samples = 0;
uint32_t next_frame_samples = 0;
bool accepts_blocks = true;
uint32_t sample_rate = 0;
uint64_t sample_phase = 0;
uint64_t sample_step = 0;
uint64_t decoded_samples = 0;
uint64_t decoded_bytes = 0;
uint32_t duration_remaining = 0;
uint32_t skip_remaining = 0;
const uint8_t* waveform_end = nullptr;
std::vector<Ngs2SamplerFilter> filters;
const uint8_t* WaveformData() const {
@@ -556,28 +565,44 @@ struct Ngs2VoiceInternal {
channels * sizeof(int16_t));
}
void SetupSampler(const Ngs2WaveformFormat& format) {
EXIT_NOT_IMPLEMENTED(format.frame_margin != 0 || format.frame_offset != 0 ||
format.sample_rate == 0 || format.num_channels == 0 ||
(format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9 &&
format.sample_rate != rack->ngs->option.sample_rate));
void ResetSetupState() {
SetEvent(4);
std::ranges::fill(ports, Port {});
for (auto& matrix: matrices) {
matrix.clear();
}
channels = format.num_channels;
sample_rate = format.sample_rate;
sample_phase = 0;
sample_step = uint64_t(sample_rate) << 32u;
decoded_samples = 0;
decoded_bytes = 0;
waveform_end = nullptr;
blocks.clear();
filters.clear();
}
void SetupSampler(const Ngs2WaveformFormat& format) {
const bool deferred = format.waveform_type == 0 && format.num_channels == 0 &&
format.sample_rate == 0 && format.config_data == 0 &&
format.frame_margin == 0 && format.frame_offset == 0;
EXIT_NOT_IMPLEMENTED(!deferred &&
(format.frame_margin != 0 || format.frame_offset != 0 ||
format.sample_rate == 0 || format.num_channels == 0));
ResetSetupState();
channels = format.num_channels;
sample_rate = format.sample_rate;
sample_phase = 0;
sample_step = uint64_t(sample_rate) << 32u;
decoded_samples = 0;
decoded_bytes = 0;
duration_remaining = 0;
skip_remaining = 0;
waveform_end = nullptr;
blocks.clear();
decoder.reset();
decoded_frame.clear();
next_decoded_frame.clear();
compressed_input.clear();
frame_cursor = 0;
frame_samples = 0;
next_frame_samples = 0;
accepts_blocks = true;
if (deferred) {
return;
}
if (format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) {
decoder = std::make_unique<Ajm::AjmAt9Decoder>(channels, format.sample_rate,
Ajm::AjmSampleEncoding::Float, 0);
@@ -594,6 +619,7 @@ struct Ngs2VoiceInternal {
decoder->WriteCodecInfo(&info, sizeof(info), result);
EXIT_NOT_IMPLEMENTED(info.frame_samples > std::numeric_limits<uint16_t>::max());
decoded_frame.resize(info.frame_samples * channels);
compressed_input.reserve(info.super_frame_size);
} else {
EXIT_NOT_IMPLEMENTED(format.waveform_type != 0x12);
}
@@ -653,6 +679,13 @@ struct Ngs2VoiceEventParam {
uint32_t event_id;
};
struct Ngs2SubmixerVoiceSetupParam {
Ngs2VoiceParamHeader header;
uint32_t num_io_channels;
uint32_t flags;
};
static_assert(sizeof(Ngs2SubmixerVoiceSetupParam) == 16);
struct Ngs2VoicePatchParam {
Ngs2VoiceParamHeader header;
uint32_t port;
@@ -749,6 +782,8 @@ static_assert(sizeof(Ngs2CustomMasteringVoiceState) == 16);
static_assert(sizeof(Ngs2SamplerVoiceState) == 56);
static_assert(sizeof(Ngs2CustomSamplerVoiceState) == 48);
static_assert(sizeof(Ngs2WaveformFormat) == 24);
static_assert(offsetof(Ngs2WaveformFormat, frame_offset) == 16 &&
offsetof(Ngs2WaveformFormat, frame_margin) == 20);
static_assert(sizeof(Ngs2WaveformBlock) == 40);
static_assert(sizeof(Ngs2WaveformInfo) == 232);
@@ -1383,10 +1418,12 @@ struct Ngs2VoiceCallbackInfo {
uint32_t flag, reserved;
uintptr_t user;
const void* block_data;
size_t size;
uint32_t repeats, attributes;
uint32_t size, repeats, attributes, reserved2;
};
static_assert(sizeof(Ngs2VoiceCallbackInfo) == 56);
static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 &&
offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 &&
offsetof(Ngs2VoiceCallbackInfo, attributes) == 48);
static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
auto& block = voice.blocks.front();
@@ -1405,8 +1442,9 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
0,
block.info.user_data,
block.data,
block.info.data_size,
static_cast<uint32_t>(block.info.data_size),
block.repeated_count,
0,
0};
if (!repeat) {
voice.blocks.pop_front();
@@ -1439,9 +1477,123 @@ static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) {
}
}
static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
uint32_t& frame_samples) {
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
auto& block = voice.blocks.front();
if (block.info.num_samples != 0 && block.data_cursor == 0) {
voice.duration_remaining = block.info.num_samples;
voice.skip_remaining = block.info.num_skip_samples;
}
if (voice.duration_remaining == 0) {
Ngs2FinishBlock(voice);
continue;
}
const auto skip =
std::min(voice.skip_remaining,
static_cast<uint32_t>(frame.size() / voice.channels));
Ajm::AjmGaplessState gapless;
gapless.Set({voice.duration_remaining, static_cast<uint16_t>(skip), 0}, true);
if (!voice.compressed_input.empty()) {
Ajm::AjmSidebandDecAt9CodecInfo info {};
voice.decoder->WriteCodecInfo(&info, sizeof(info), {});
EXIT_NOT_IMPLEMENTED(info.next_frame_size < voice.compressed_input.size());
const auto bytes =
std::min<size_t>(info.next_frame_size - voice.compressed_input.size(),
block.info.data_size - block.data_cursor);
voice.compressed_input.insert(voice.compressed_input.end(),
block.data + block.data_cursor,
block.data + block.data_cursor + bytes);
block.data_cursor += bytes;
voice.decoded_bytes += bytes;
if (voice.compressed_input.size() < info.next_frame_size) {
Ngs2FinishBlock(voice);
continue;
}
}
const bool pending = !voice.compressed_input.empty();
const auto result = voice.decoder->Decode(
pending ? voice.compressed_input.data() : block.data + block.data_cursor,
pending ? voice.compressed_input.size() : block.info.data_size - block.data_cursor,
frame.data(), frame.size() * sizeof(float), false,
&gapless);
if (!pending && result.result == Ajm::AJM_RESULT_PARTIAL_INPUT &&
result.input_consumed == 0) {
voice.compressed_input.insert(voice.compressed_input.end(),
block.data + block.data_cursor,
block.data + block.info.data_size);
voice.decoded_bytes += block.info.data_size - block.data_cursor;
block.data_cursor = block.info.data_size;
Ngs2FinishBlock(voice);
continue;
}
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
if (pending) {
voice.compressed_input.erase(voice.compressed_input.begin(),
voice.compressed_input.begin() + result.input_consumed);
} else {
block.data_cursor += result.input_consumed;
voice.decoded_bytes += result.input_consumed;
}
voice.skip_remaining -= skip - gapless.current.skip_samples;
const auto decoded_samples =
static_cast<uint32_t>(result.output_written / (sizeof(float) * voice.channels));
if (decoded_samples != 0) {
EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining);
frame_samples = decoded_samples;
voice.duration_remaining -= decoded_samples;
}
if (voice.duration_remaining == 0 ||
(block.data_cursor == block.info.data_size && voice.compressed_input.empty())) {
if (voice.duration_remaining == 0) {
voice.compressed_input.clear();
}
Ngs2FinishBlock(voice);
}
if (decoded_samples != 0) {
return true;
}
}
return false;
}
static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
if (voice.frame_cursor == voice.frame_samples) {
if (voice.next_frame_samples != 0) {
std::swap(voice.decoded_frame, voice.next_decoded_frame);
voice.frame_samples = voice.next_frame_samples;
voice.next_frame_samples = 0;
} else if (!Ngs2DecodeFrame(voice, voice.decoded_frame, voice.frame_samples)) {
return false;
}
voice.frame_cursor = 0;
}
return voice.state == Ngs2VoicePlayState::Playing &&
voice.frame_cursor < voice.frame_samples;
}
static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
while (voice.state == Ngs2VoicePlayState::Playing && voice.sample_phase >= output_rate) {
if (!Ngs2CurrentAtracFrame(voice)) {
break;
}
++voice.frame_cursor;
++voice.decoded_samples;
voice.sample_phase -= output_rate;
}
if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples &&
voice.next_frame_samples == 0 && !voice.accepts_blocks) {
voice.sample_phase = 0;
}
}
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
uint32_t output = 0;
while (voice.state == Ngs2VoicePlayState::Playing && output < grain && !voice.blocks.empty()) {
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
while (voice.state == Ngs2VoicePlayState::Playing && output < grain &&
(!voice.blocks.empty() || voice.frame_cursor < voice.frame_samples ||
voice.next_frame_samples != 0)) {
if (voice.decoder == nullptr) {
Ngs2AdvancePcm(voice);
if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) {
@@ -1461,8 +1613,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
next_block.info.num_skip_samples * voice.channels;
}
const float fraction = static_cast<float>(voice.sample_phase) /
static_cast<float>(uint64_t(voice.rack->ngs->option.sample_rate)
<< 32u);
static_cast<float>(output_rate);
for (uint32_t c = 0; c < voice.channels; ++c) {
voice.samples[c * grain + output] =
std::lerp(static_cast<float>(pcm[c]), static_cast<float>(next[c]), fraction) /
@@ -1472,44 +1623,39 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
++output;
continue;
}
auto& block = voice.blocks.front();
auto count = std::min(grain - output, block.info.num_samples - block.cursor);
const auto skip =
std::min(block.skip_remaining,
static_cast<uint32_t>(voice.decoded_frame.size() / voice.channels));
Ajm::AjmGaplessState gapless;
gapless.Set({block.info.num_samples - block.cursor, static_cast<uint16_t>(skip), 0},
true);
const auto result = voice.decoder->Decode(
block.data + block.data_cursor, block.info.data_size - block.data_cursor,
voice.decoded_frame.data(), voice.decoded_frame.size() * sizeof(float), false,
&gapless);
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
block.data_cursor += result.input_consumed;
block.skip_remaining -= skip - gapless.current.skip_samples;
const auto decoded_samples =
static_cast<uint32_t>(result.output_written / (sizeof(float) * voice.channels));
if (decoded_samples == 0) {
continue;
Ngs2AdvanceAtrac9(voice);
if (!Ngs2CurrentAtracFrame(voice)) {
break;
}
EXIT_NOT_IMPLEMENTED(decoded_samples > count);
count = decoded_samples;
for (uint32_t c = 0; c < voice.channels; ++c) {
for (uint32_t i = 0; i < count; ++i) {
const auto index = i * voice.channels + c;
voice.samples[c * grain + output + i] = voice.decoded_frame[index];
if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) {
if (voice.next_frame_samples == 0 && !voice.blocks.empty()) {
if (voice.next_decoded_frame.empty()) {
voice.next_decoded_frame.resize(voice.decoded_frame.size());
}
Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples);
}
if (voice.state != Ngs2VoicePlayState::Playing ||
voice.frame_cursor >= voice.frame_samples) {
break;
}
}
output += count;
block.cursor += count;
voice.decoded_samples += count;
if (block.cursor == block.info.num_samples) {
voice.decoded_bytes += block.info.data_size;
Ngs2FinishBlock(voice);
const float* current = voice.decoded_frame.data() + voice.frame_cursor * voice.channels;
const float* next = voice.frame_cursor + 1 < voice.frame_samples
? current + voice.channels
: voice.next_frame_samples != 0 ? voice.next_decoded_frame.data()
: current;
const float fraction = static_cast<float>(voice.sample_phase) /
static_cast<float>(output_rate);
for (uint32_t c = 0; c < voice.channels; ++c) {
voice.samples[c * grain + output] = std::lerp(current[c], next[c], fraction);
}
voice.sample_phase += voice.sample_step;
++output;
}
if (voice.decoder == nullptr) {
Ngs2AdvancePcm(voice);
} else {
Ngs2AdvanceAtrac9(voice);
}
voice.has_samples = output != 0;
}
@@ -1519,6 +1665,11 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
if (voice.rendered) {
return;
}
if (voice.channels == 0) {
voice.has_samples = false;
voice.rendered = true;
return;
}
EXIT_NOT_IMPLEMENTED(voice.rendering);
voice.rendering = true;
voice.samples.assign(grain * voice.channels, 0.0f);
@@ -1535,6 +1686,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
}
if (voice.state == Ngs2VoicePlayState::Playing && voice.blocks.empty() &&
voice.frame_cursor == voice.frame_samples &&
voice.next_frame_samples == 0 &&
!voice.accepts_blocks &&
std::ranges::none_of(voice.filters,
[](const auto& filter) { return filter.HasHistory(); })) {
@@ -1552,12 +1705,16 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
EXIT_NOT_IMPLEMENTED(port.input != 0);
const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
for (uint32_t dst = 0; dst < voice.channels; ++dst) {
const size_t output_channels =
matrix == nullptr
? voice.channels
: std::min<size_t>(voice.channels, matrix->size() / source->channels);
for (size_t dst = 0; dst < output_channels; ++dst) {
for (uint32_t src = 0; src < source->channels; ++src) {
const float level =
port.volume * (matrix == nullptr
? (src == dst ? 1.0f : 0.0f)
: matrix->at(dst * source->channels + src));
: (*matrix)[dst * source->channels + src]);
for (uint32_t i = 0; i < grain; ++i) {
voice.samples[dst * grain + i] +=
source->samples[src * grain + i] * level;
@@ -1612,6 +1769,7 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
EXIT_NOT_IMPLEMENTED(buffer_info == nullptr || system_handle == 0 || num_buffer_info == 0);
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
Common::LockGuard lock(ngs->mutex);
std::vector<std::vector<float>> output_mix(num_buffer_info);
std::vector<Ngs2VoiceInternal*> voices;
{
Common::LockGuard racks_lock(g_racks_mutex);
@@ -1633,9 +1791,6 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
}
const auto grain = ngs->option.num_grain_samples;
for (auto* voice: voices) {
if (voice->channels == 0) {
continue;
}
Ngs2RenderVoice(*voice, voices, grain);
if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) {
continue;
@@ -1643,10 +1798,21 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info);
const auto& output = buffer_info[voice->output_id];
EXIT_NOT_IMPLEMENTED(output.buffer == nullptr);
EXIT_NOT_IMPLEMENTED(output.waveform_type != 0x18 ||
EXIT_NOT_IMPLEMENTED((output.waveform_type != 0x12 && output.waveform_type != 0x18) ||
output.num_channels != voice->channels);
EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * sizeof(float));
auto* pcm = static_cast<float*>(output.buffer);
EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels *
(output.waveform_type == 0x12 ? sizeof(int16_t)
: sizeof(float)));
float* pcm = nullptr;
if (output.waveform_type == 0x12) {
auto& mix = output_mix[voice->output_id];
if (mix.empty()) {
mix.resize(grain * output.num_channels);
}
pcm = mix.data();
} else {
pcm = static_cast<float*>(output.buffer);
}
for (uint32_t dst = 0; dst < output.num_channels; ++dst) {
for (uint32_t src = 0; src < voice->channels; ++src) {
const float level = voice->output_matrix.empty()
@@ -1658,6 +1824,18 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
}
}
}
for (uint32_t i = 0; i < num_buffer_info; ++i) {
const auto& mix = output_mix[i];
if (mix.empty()) {
continue;
}
auto* pcm = static_cast<int16_t*>(buffer_info[i].buffer);
for (size_t sample = 0; sample < mix.size(); ++sample) {
pcm[sample] = static_cast<int16_t>(std::clamp(
std::lrintf(std::clamp(mix[sample], -1.0f, 1.0f) * 32768.0f), -32768l,
32767l));
}
}
for (auto* voice: voices) {
// No envelope is configured, so a release stop finishes in this render.
if (voice->state == Ngs2VoicePlayState::Stopped) {
@@ -1682,6 +1860,23 @@ static bool Ngs2FourCcEquals(const uint8_t* data, const char* four_cc) {
return std::memcmp(data, four_cc, 4) == 0;
}
static bool Ngs2GetAtrac9CodecInfo(std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE> config,
Atrac9CodecInfo& codec) {
if (config[0] != 0xfe || (config[1] & 1u) != 0 || ((config[1] >> 1u) & 7u) >= 6u) {
return false;
}
void* decoder = Atrac9GetHandle();
const bool valid = decoder != nullptr && Atrac9InitDecoder(decoder, config.data()) == 0 &&
Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 &&
codec.samplingRate > 0 && codec.superframeSize > 0 &&
codec.framesInSuperframe > 0 && codec.frameSamples > 0 &&
codec.superframeSize % codec.framesInSuperframe == 0;
if (decoder != nullptr) {
Atrac9ReleaseHandle(decoder);
}
return valid;
}
static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformInfo* info) {
static constexpr uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36,
0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65,
@@ -1695,7 +1890,7 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
}
const uint64_t riff_end64 = 8ull + Ngs2ReadLe32(bytes + 4);
if (riff_end64 < 12 || riff_end64 > data_size) {
if (riff_end64 < 12) {
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
}
const auto riff_end = static_cast<size_t>(riff_end64);
@@ -1705,11 +1900,13 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
size_t waveform_offset = 0;
uint32_t waveform_size = 0;
for (size_t offset = 12; offset + 8 <= riff_end;) {
for (size_t offset = 12; offset + 8 <= std::min(riff_end, data_size);) {
const auto* chunk = bytes + offset;
const auto chunk_size = static_cast<size_t>(Ngs2ReadLe32(chunk + 4));
const auto payload = offset + 8;
if (chunk_size > riff_end - payload) {
const auto is_data = Ngs2FourCcEquals(chunk, "data");
const uint64_t next = static_cast<uint64_t>(payload) + chunk_size + (chunk_size & 1u);
if (next > riff_end || (next > data_size && !is_data)) {
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
}
@@ -1723,7 +1920,7 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
}
fact = bytes + payload;
} else if (Ngs2FourCcEquals(chunk, "data")) {
} else if (is_data) {
if (payload > std::numeric_limits<uint32_t>::max()) {
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
}
@@ -1731,9 +1928,8 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
waveform_size = static_cast<uint32_t>(chunk_size);
}
const uint64_t next = static_cast<uint64_t>(payload) + chunk_size + (chunk_size & 1u);
if (next > riff_end) {
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
if (next > data_size) {
break;
}
offset = static_cast<size_t>(next);
}
@@ -1748,20 +1944,8 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE> config {};
std::memcpy(config.data(), format + 44, config.size());
if (config[0] != 0xfe || (config[1] & 1u) != 0 || ((config[1] >> 1u) & 7u) >= 6u) {
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
}
Atrac9CodecInfo codec {};
void* decoder = Atrac9GetHandle();
const bool valid_codec =
decoder != nullptr && Atrac9InitDecoder(decoder, config.data()) == 0 &&
Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 &&
codec.samplingRate > 0 && codec.superframeSize > 0 && codec.framesInSuperframe > 0 &&
codec.frameSamples > 0 && codec.superframeSize % codec.framesInSuperframe == 0;
if (decoder != nullptr) {
Atrac9ReleaseHandle(decoder);
}
const bool valid_codec = Ngs2GetAtrac9CodecInfo(config, codec);
const uint64_t frame_samples =
static_cast<uint64_t>(codec.frameSamples) * static_cast<uint64_t>(codec.framesInSuperframe);
if (!valid_codec || codec.channels != Ngs2ReadLe16(format + 2) ||
@@ -1819,6 +2003,32 @@ int KYTY_SYSV_ABI Ngs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32
EXIT_NOT_IMPLEMENTED(block == nullptr);
std::memset(block, 0, sizeof(Ngs2WaveformBlock));
if (format == nullptr || format->num_channels == 0 || format->num_channels > 8 ||
format->sample_rate == 0 || format->sample_rate > 192000) {
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
}
if (format->waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) {
std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE> config {
static_cast<uint8_t>(format->config_data >> 24u),
static_cast<uint8_t>(format->config_data >> 16u),
static_cast<uint8_t>(format->config_data >> 8u),
static_cast<uint8_t>(format->config_data)};
Atrac9CodecInfo codec {};
if (!Ngs2GetAtrac9CodecInfo(config, codec) ||
codec.channels != format->num_channels || codec.samplingRate != format->sample_rate) {
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
}
const uint64_t frame_samples = uint64_t(codec.frameSamples) * codec.framesInSuperframe;
const uint64_t skip = sample_pos % frame_samples;
block->data_offset = uint64_t(sample_pos / frame_samples) * codec.superframeSize;
block->data_size = num_samples == 0
? 0
: ((skip + num_samples + frame_samples - 1) / frame_samples) *
codec.superframeSize;
block->num_skip_samples = num_samples == 0 ? 0 : static_cast<uint32_t>(skip);
} else {
return NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT;
}
block->num_samples = num_samples;
return OK;
}
@@ -2076,7 +2286,21 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
}
break;
}
case 0x2000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); break;
case 0x2000: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer);
if ((param->id & 0xffffu) == 0) {
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2SubmixerVoiceSetupParam));
const auto& setup =
*reinterpret_cast<const Ngs2SubmixerVoiceSetupParam*>(param);
EXIT_NOT_IMPLEMENTED(
setup.num_io_channels == 0 ||
setup.num_io_channels > voice->rack->option.submixer.max_channels ||
setup.flags != 0);
voice->ResetSetupState();
voice->channels = setup.num_io_channels;
}
break;
}
case 0x2001: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Reverb); break;
case 0x3000: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering);
@@ -2145,18 +2369,32 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
};
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
(blocks.flags != 0 && blocks.flags != 1 &&
blocks.flags != 0x11 &&
blocks.flags != 0x4));
if (blocks.flags == 0x4) {
(blocks.flags & ~0x17u) != 0);
if ((blocks.flags & 0x4u) != 0) {
voice->blocks.clear();
voice->sample_phase = 0;
voice->waveform_end = nullptr;
voice->sample_phase = 0;
voice->duration_remaining = 0;
voice->skip_remaining = 0;
voice->compressed_input.clear();
voice->frame_cursor = 0;
voice->frame_samples = 0;
voice->next_frame_samples = 0;
voice->waveform_end = nullptr;
}
voice->accepts_blocks = (blocks.flags & 1u) != 0;
const bool only_data = (blocks.flags & 2u) != 0;
EXIT_NOT_IMPLEMENTED(only_data && voice->decoder == nullptr);
for (uint32_t i = 0; i < blocks.count; ++i) {
const auto& block = blocks.blocks[i];
EXIT_NOT_IMPLEMENTED(block.num_samples == 0);
auto block = blocks.blocks[i];
if (!only_data && block.num_samples == 0 && block.data_size == 0) {
continue;
}
EXIT_NOT_IMPLEMENTED(!only_data && block.num_samples == 0);
if (only_data) {
block.num_repeats = 0;
block.num_skip_samples = 0;
block.num_samples = 0;
}
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 &&
(voice->rack->type != Ngs2RackType::Sampler ||
voice->decoder != nullptr));
@@ -2179,12 +2417,12 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
}
break;
}
case 5: {
const float ratio = *reinterpret_cast<const float*>(param + 1);
EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio) || ratio < 0.0f || ratio > 4.0f ||
(voice->decoder != nullptr && ratio != 1.0f));
case 5: {
const float ratio = *reinterpret_cast<const float*>(param + 1);
EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio));
const float clamped_ratio = std::clamp(ratio, 0.0f, 4.0f);
voice->sample_step = static_cast<uint64_t>(std::llround(
static_cast<double>(voice->sample_rate) * ratio * 4294967296.0));
static_cast<double>(voice->sample_rate) * clamped_ratio * 4294967296.0));
break;
}
case 0xa: {
-47
View File
@@ -7,53 +7,6 @@ namespace Loader::Jit {
#pragma pack(1)
struct JmpWithIndex {
void SetIndex(uint32_t index) { *reinterpret_cast<uint32_t*>(&code[1]) = index; }
void SetFunc(void* handler) {
auto func_addr = reinterpret_cast<int64_t>(handler);
auto rip_addr = reinterpret_cast<int64_t>(&code[10]);
auto offset64 = func_addr - rip_addr;
auto offset32 = static_cast<uint32_t>(static_cast<uint64_t>(offset64) & 0xffffffffu);
*reinterpret_cast<uint32_t*>(&code[6]) = offset32;
}
static uint64_t GetSize() { return 16; }
// 68 00 00 00 00 push <index>
// E9 E0 FF FF FF jmp <handler>
uint8_t code[16] = {0x68, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00,
0x00, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
};
struct CallPlt {
explicit CallPlt(uint32_t table_size) {
for (uint32_t index = 0; index < table_size; index++) {
auto* c = new (&code[32] + JmpWithIndex::GetSize() * index) JmpWithIndex;
c->SetIndex(index);
c->SetFunc(this);
}
}
void SetPltGot(uint64_t vaddr) { *reinterpret_cast<uint64_t*>(&code[2]) = vaddr; }
uint64_t GetAddr(uint32_t index) {
return reinterpret_cast<uint64_t>(&code[32] + JmpWithIndex::GetSize() * index);
}
static uint64_t GetSize(uint32_t table_size) {
return 32 + JmpWithIndex::GetSize() * table_size;
}
// 0: 49 bb 88 77 66 55 44 movabs r11,0x1122334455667788
// 7: 33 22 11
// a: 41 ff 73 08 push QWORD PTR [r11+0x8]
// e: 41 ff 63 10 jmp QWORD PTR [r11+0x10]
uint8_t code[32] = {0x49, 0xBB, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x41, 0xFF,
0x73, 0x08, 0x41, 0xFF, 0x63, 0x10, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
};
struct JmpRax {
template <class Handler>
void SetFunc(Handler func) {
+122 -375
View File
@@ -126,19 +126,13 @@ struct StubbedImportRecord {
std::string program;
};
// The structure will be passed via the stack
// since the size of an object is larger than 16 bytes
struct RelocateHandlerStack {
uint64_t stack[3];
};
static std::vector<StubbedImportRecord> g_stubbed_imports;
static std::atomic_uint32_t g_unresolved_stub_call_log_count {0};
static std::vector<uint64_t> g_unresolved_stub_thunk_pages;
static uint64_t g_unresolved_stub_thunk_offset = 0;
static constexpr uint64_t UNRESOLVED_STUB_PAGE_SIZE = 4096;
static KYTY_SYSV_ABI uint64_t ResolveImportStubWithId(uint64_t record_id);
static KYTY_SYSV_ABI uint64_t UnresolvedImportStub(uint64_t record_id);
static bool PatchGuestMemory64(uint64_t vaddr, uint64_t value) {
auto* ptr = reinterpret_cast<uint64_t*>(vaddr);
@@ -148,7 +142,7 @@ static bool PatchGuestMemory64(uint64_t vaddr, uint64_t value) {
}
static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
constexpr uint64_t thunk_size = 165;
constexpr uint64_t thunk_size = 34;
if (g_unresolved_stub_thunk_pages.empty() ||
g_unresolved_stub_thunk_offset + thunk_size > UNRESOLVED_STUB_PAGE_SIZE) {
@@ -164,105 +158,18 @@ static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
g_unresolved_stub_thunk_offset);
g_unresolved_stub_thunk_offset += thunk_size;
const auto target = reinterpret_cast<uint64_t>(ResolveImportStubWithId);
uint8_t bytes[thunk_size] {};
size_t i = 0;
const auto emit = [&](uint8_t b) { bytes[i++] = b; };
const auto emit64 = [&](uint64_t v) {
std::memcpy(bytes + i, &v, sizeof(v));
i += sizeof(v);
uint8_t bytes[thunk_size] = {
0x48, 0x83, 0xec, 0x08, // sub rsp, 8
0x48, 0xbf, 0, 0, 0, 0, 0, 0, 0, 0, // mov rdi, record_id
0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, // mov rax, UnresolvedImportStub
0xff, 0xd0, // call rax
0x48, 0x83, 0xc4, 0x08, // add rsp, 8
0x0f, 0x57, 0xc0, // xorps xmm0, xmm0
0xc3, // ret
};
const auto emit32 = [&](uint32_t v) {
std::memcpy(bytes + i, &v, sizeof(v));
i += sizeof(v);
};
const auto save_xmm = [&](uint8_t reg, uint8_t offset) {
emit(0xf3);
emit(0x0f);
emit(0x7f);
if (offset == 0) {
emit(static_cast<uint8_t>(0x04u | (reg << 3u)));
emit(0x24);
} else {
emit(static_cast<uint8_t>(0x44u | (reg << 3u)));
emit(0x24);
emit(offset);
}
};
const auto load_xmm = [&](uint8_t reg, uint8_t offset) {
emit(0xf3);
emit(0x0f);
emit(0x6f);
if (offset == 0) {
emit(static_cast<uint8_t>(0x04u | (reg << 3u)));
emit(0x24);
} else {
emit(static_cast<uint8_t>(0x44u | (reg << 3u)));
emit(0x24);
emit(offset);
}
};
emit(0x50); // push rax; preserve AL for variadic SysV calls
emit(0x57); // push rdi
emit(0x56); // push rsi
emit(0x52); // push rdx
emit(0x51); // push rcx
emit(0x41);
emit(0x50); // push r8
emit(0x41);
emit(0x51); // push r9
emit(0x48);
emit(0x81);
emit(0xec);
emit32(0x80); // sub rsp, 0x80
for (uint8_t reg = 0; reg < 8; reg++) {
save_xmm(reg, static_cast<uint8_t>(reg * 0x10u));
}
emit(0x48);
emit(0xbf);
emit64(record_id); // mov rdi, record_id
emit(0x48);
emit(0xb8);
emit64(target); // mov rax, ResolveImportStubWithId
emit(0xff);
emit(0xd0); // call rax
emit(0x49);
emit(0x89);
emit(0xc3); // mov r11, rax
for (uint8_t reg = 0; reg < 8; reg++) {
load_xmm(reg, static_cast<uint8_t>(reg * 0x10u));
}
emit(0x48);
emit(0x81);
emit(0xc4);
emit32(0x80); // add rsp, 0x80
emit(0x41);
emit(0x59); // pop r9
emit(0x41);
emit(0x58); // pop r8
emit(0x59); // pop rcx
emit(0x5a); // pop rdx
emit(0x5e); // pop rsi
emit(0x5f); // pop rdi
emit(0x58); // pop rax
emit(0x4d);
emit(0x85);
emit(0xdb); // test r11, r11
emit(0x74);
emit(0x03); // jz +3
emit(0x41);
emit(0xff);
emit(0xe3); // jmp r11
// Match the integer fallback for floating-point return values.
emit(0x0f);
emit(0x57);
emit(0xc0); // xorps xmm0, xmm0
emit(0x31);
emit(0xc0); // xor eax, eax
emit(0xc3); // ret
EXIT_NOT_IMPLEMENTED(i != thunk_size);
const auto target = reinterpret_cast<uint64_t>(UnresolvedImportStub);
std::memcpy(bytes + 6, &record_id, sizeof(record_id));
std::memcpy(bytes + 16, &target, sizeof(target));
std::memcpy(code, bytes, sizeof(bytes));
Common::VirtualMemory::FlushInstructionCache(reinterpret_cast<uint64_t>(code), thunk_size);
return reinterpret_cast<uint64_t>(code);
@@ -270,15 +177,8 @@ static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
const RelocationInfo& ri) {
const auto program_name = program != nullptr ? Common::PathToString(program->file_name) : "";
for (auto& record: g_stubbed_imports) {
for (const auto& record: g_stubbed_imports) {
if (record.patch_vaddr == ri.vaddr) {
record.index = index;
record.name = ri.name;
record.type = ri.type;
record.bind = ri.bind;
record.program = program_name;
return record.thunk_vaddr;
}
}
@@ -289,7 +189,7 @@ static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
record.name = ri.name;
record.type = ri.type;
record.bind = ri.bind;
record.program = program_name;
record.program = Common::PathToString(program->file_name);
g_stubbed_imports.push_back(record);
const auto record_id = g_stubbed_imports.size() - 1;
const auto thunk = AllocateUnresolvedImportThunk(record_id);
@@ -297,31 +197,7 @@ static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
return thunk;
}
static KYTY_SYSV_ABI uint64_t ResolveImportStubWithId(uint64_t record_id) {
if (record_id < g_stubbed_imports.size()) {
auto& record = g_stubbed_imports[record_id];
auto nid = record.name;
auto pos = nid.find('[');
if (pos != std::string::npos) {
nid.resize(pos);
}
SymbolRecord resolved {};
if (!nid.empty() &&
Common::Singleton<RuntimeLinker>::Instance()->ResolveLoadedSymbolByNid(nid, record.type,
&resolved) &&
resolved.vaddr != 0 && resolved.vaddr != record.thunk_vaddr) {
LOGF("Late-resolved import: %s -> %s [0x%016" PRIx64 "]\n", record.name.c_str(),
resolved.name.c_str(), resolved.vaddr);
if (record.patch_vaddr != 0) {
PatchGuestMemory64(record.patch_vaddr, resolved.vaddr);
}
return resolved.vaddr;
}
}
static KYTY_SYSV_ABI uint64_t UnresolvedImportStub(uint64_t record_id) {
const auto log_index = g_unresolved_stub_call_log_count.fetch_add(1);
if (log_index < 1024) {
if (record_id < g_stubbed_imports.size()) {
@@ -1007,135 +883,56 @@ static RelocationInfo GetRelocationInfo(Elf64_Rela* r, Program* program) {
return ret;
}
static void RelocateRecord(uint32_t index, Elf64_Rela* r, Program* program, bool jmprela_table,
bool imports_only, std::vector<std::string>* unresolved) {
static bool RelocateRecord(uint32_t index, Elf64_Rela* r, Program* program, bool jmprela_table) {
KYTY_PROFILER_FUNCTION();
auto ri = GetRelocationInfo(r, program);
if (imports_only &&
(ri.bind_self || (ri.bind != BindType::Global && ri.bind != BindType::Weak))) {
return;
}
[[maybe_unused]] bool patched = false;
bool stubbed_import = false;
bool stubbed_func = false;
// KYTY_PROFILER_BLOCK("patch");
if (ri.resolved) {
patched = PatchGuestMemory64(ri.vaddr, ri.value);
} else {
uint64_t value = 0;
bool weak = (ri.bind == BindType::Weak || !program->fail_if_global_not_resolved);
if (ri.type == SymbolType::Object && weak) {
const auto ri = GetRelocationInfo(r, program);
auto value = ri.value;
bool stubbed = false;
if (!ri.resolved) {
const bool weak = ri.bind == BindType::Weak || !program->fail_if_global_not_resolved;
if (!weak) {
LOGF("Stubbed: %s\n",
fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value, ri.name,
magic_enum::enum_name(ri.type), magic_enum::enum_name(ri.bind),
ri.dbg_name).c_str());
}
if (ri.type == SymbolType::Object) {
value = g_invalid_memory;
} else if (ri.type == SymbolType::Func && jmprela_table && weak) {
value = RegisterStubbedImport(index, program, ri);
stubbed_import = true;
stubbed_func = true;
} else if (ri.type == SymbolType::Func && !jmprela_table && weak) {
value = RegisterStubbedImport(index, program, ri);
stubbed_func = true;
} else if (ri.type == SymbolType::NoType && weak) {
value = RuntimeLinker::ReadFromElf(program, ri.vaddr) + ri.base_vaddr;
}
if (value != 0) {
patched = PatchGuestMemory64(ri.vaddr, value);
} else {
auto dbg_str = fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value,
ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), ri.dbg_name.c_str());
if (unresolved != nullptr) {
unresolved->push_back(dbg_str);
} else {
EXIT("Can't resolve: %s\n", dbg_str.c_str());
}
if (ri.type == SymbolType::Object) {
value = g_invalid_memory;
} else if (ri.type == SymbolType::Func || ri.type == SymbolType::NoType) {
value = RegisterStubbedImport(index, program, ri);
stubbed_func = true;
if (jmprela_table) {
stubbed_import = true;
}
}
if (value != 0) {
patched = PatchGuestMemory64(ri.vaddr, value);
}
value = RegisterStubbedImport(index, program, ri);
stubbed = true;
}
}
// KYTY_PROFILER_END_BLOCK;
if (patched && stubbed_import) {
const auto thunk = RegisterStubbedImport(index, program, ri);
LOGF("Relocate: unresolved PLT import patched to stub [%u] [%016" PRIx64 "] <- %016" PRIx64
const bool patched = PatchGuestMemory64(ri.vaddr, value);
if (patched && stubbed) {
LOGF("Relocate: unresolved %s import patched to stub [%u] [%016" PRIx64 "] <- %016" PRIx64
", %s, %s, %s, %s\n",
index, ri.vaddr, thunk, ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), Common::PathToString(program->file_name).c_str());
} else if (patched && stubbed_func) {
const auto thunk = RegisterStubbedImport(index, program, ri);
LOGF("Relocate: unresolved non-PLT function patched to stub [%u] [%016" PRIx64
"] <- %016" PRIx64 ", %s, %s, %s, %s\n",
index, ri.vaddr, thunk, ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), Common::PathToString(program->file_name).c_str());
jmprela_table ? "PLT" : "non-PLT", index, ri.vaddr, value, ri.name.c_str(),
magic_enum::enum_name(ri.type), magic_enum::enum_name(ri.bind),
Common::PathToString(program->file_name).c_str());
}
if (program->dbg_print_reloc) {
if (patched && !ri.bind_self &&
(ri.bind == BindType::Global || ri.bind == BindType::Weak ||
ri.type == SymbolType::TlsModule)) {
auto dbg_str = fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value,
ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), ri.dbg_name.c_str());
LOGF("Relocate: %s\n", dbg_str.c_str());
}
if (program->dbg_print_reloc && patched && !ri.bind_self &&
(ri.bind == BindType::Global || ri.bind == BindType::Weak ||
ri.type == SymbolType::TlsModule)) {
LOGF("Relocate: %s\n",
fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value, ri.name,
magic_enum::enum_name(ri.type), magic_enum::enum_name(ri.bind),
ri.dbg_name).c_str());
}
return ri.resolved;
}
static void RelocateRecords(Elf64_Rela* records, uint64_t size, Program* program,
bool jmprela_table, bool imports_only,
std::vector<std::string>* unresolved) {
KYTY_PROFILER_FUNCTION();
uint32_t index = 0;
for (auto* r = records;
reinterpret_cast<uint8_t*>(r) < reinterpret_cast<uint8_t*>(records) + size; r++, index++) {
RelocateRecord(index, r, program, jmprela_table, imports_only, unresolved);
static void ForEachRelocation(Program* program, auto&& func) {
const auto& info = *program->dynamic_info;
for (uint32_t i = 0; i < info.rela_table_total_size / sizeof(Elf64_Rela); ++i) {
func(info.rela_table + i, i, false);
}
}
__attribute__((naked)) static KYTY_SYSV_ABI void RelocateHandlerReturnStub() {
asm volatile("addq $8, %rsp\n\t"
"retq\n");
}
static KYTY_SYSV_ABI uint64_t RelocateHandler(RelocateHandlerStack s) {
auto* stack = s.stack;
auto* program = reinterpret_cast<Program*>(stack[-1]);
auto rel_index = stack[0];
std::string name = "<unknown function>";
if (program != nullptr && program->dynamic_info != nullptr &&
program->dynamic_info->jmprela_table != nullptr) {
auto ri = GetRelocationInfo(program->dynamic_info->jmprela_table + rel_index, program);
name = ri.name.c_str();
for (uint32_t i = 0; i < info.jmprela_table_size / sizeof(Elf64_Rela); ++i) {
func(info.jmprela_table + i, i, true);
}
// Restore return address (for stack trace)
stack[-1] = reinterpret_cast<uint64_t>(RelocateHandlerReturnStub);
LOGF("=== Stubbed function, returning OK ===\n[%d]\t%s\n", Common::Thread::GetThreadIdUnique(),
name.c_str());
return 0;
}
static KYTY_MS_ABI uint8_t* TlsMainGetAddr() {
@@ -1314,8 +1111,6 @@ void RuntimeLinker::RelocateAll() {
for (auto* p: m_programs) {
Relocate(p);
}
m_relocated = true;
}
void RuntimeLinker::RelocateProgram(Program* program) {
@@ -1335,17 +1130,52 @@ void RuntimeLinker::UnloadProgram(Program* program) {
Common::LockGuard lock(m_mutex);
if (auto it = std::find(m_programs.begin(), m_programs.end(), program);
it != m_programs.end()) {
DeleteProgram(*it);
m_programs.erase(it);
} else {
EXIT("program not found");
}
auto it = std::find(m_programs.begin(), m_programs.end(), program);
EXIT_IF(it == m_programs.end());
m_programs.erase(it);
if (m_relocated) {
RelocateAll();
for (auto* importer: m_programs) {
const auto& info = *importer->dynamic_info;
const auto rela_count =
static_cast<uint32_t>(info.rela_table_total_size / sizeof(Elf64_Rela));
ForEachRelocation(importer, [&](Elf64_Rela* r, uint32_t index, bool jmprela) {
const auto bit_index = (jmprela ? rela_count : 0) + index;
auto& bits = importer->rela_bits[bit_index >> 3u];
const auto mask = 1u << (bit_index & 7u);
const auto type = r->GetType();
if ((bits & mask) == 0 || (type != R_X86_64_64 && type != R_X86_64_GLOB_DAT &&
type != R_X86_64_JUMP_SLOT)) {
return;
}
const auto& symbol = info.symbol_table[r->GetSymbol()];
if (symbol.GetBind() != STB_GLOBAL && symbol.GetBind() != STB_WEAK) {
return;
}
const auto value =
*reinterpret_cast<const uint64_t*>(importer->base_vaddr + r->r_offset);
const auto address = value - (type == R_X86_64_64 ? r->r_addend : 0);
if (address < program->base_vaddr ||
address >= program->base_vaddr + program->mapped_size) {
return;
}
SymbolType symbol_type;
switch (symbol.GetType()) {
case STT_NOTYPE: symbol_type = SymbolType::NoType; break;
case STT_FUNC: symbol_type = SymbolType::Func; break;
case STT_OBJECT: symbol_type = SymbolType::Object; break;
default: return;
}
const std::string name = info.str_table + symbol.st_name;
if (program->export_symbols->FindByNid(name.substr(0, name.find('#')), symbol_type,
address) != nullptr) {
bits &= ~mask;
if (RelocateRecord(index, r, importer, jmprela)) {
bits |= mask;
}
}
});
}
DeleteProgram(program);
}
RuntimeLinker::RuntimeLinker(): m_symbols(std::make_unique<SymbolDatabase>()) {
@@ -1499,7 +1329,6 @@ void RuntimeLinker::Clear() {
g_tls_cached_main_tcb = nullptr;
g_desired_base_addr = SYSTEM_RESERVED + CODE_BASE_OFFSET;
m_symbols.reset();
m_relocated = false;
}
void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* program,
@@ -1559,10 +1388,20 @@ void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* p
}
if (rec == nullptr) {
if (auto* p = FindProgram(*m, *l); p != nullptr && p->export_symbols != nullptr) {
for (auto* p: m_programs) {
const auto& libs = p->dynamic_info->export_libs;
const auto& modules = p->dynamic_info->export_modules;
if (p->export_symbols == nullptr ||
std::find(libs.begin(), libs.end(), *l) == libs.end() ||
std::find(modules.begin(), modules.end(), *m) == modules.end()) {
continue;
}
rec = p->export_symbols->Find(sr);
if (bind_self != nullptr) {
*bind_self = (p == program);
if (rec != nullptr) {
if (bind_self != nullptr) {
*bind_self = (p == program);
}
break;
}
}
}
@@ -1575,7 +1414,6 @@ void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* p
}
if (rec != nullptr) {
//*out_vaddr = rec->vaddr;
*out_info = *rec;
} else {
out_info->vaddr = 0;
@@ -1598,33 +1436,6 @@ void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* p
}
}
bool RuntimeLinker::ResolveLoadedSymbolByNid(const std::string& nid, SymbolType type,
SymbolRecord* out_info) {
KYTY_PROFILER_FUNCTION();
Common::LockGuard lock(m_mutex);
EXIT_IF(out_info == nullptr);
for (auto* p: m_programs) {
if (p != nullptr && p->export_symbols != nullptr) {
if (const auto* rec = p->export_symbols->FindByNid(nid, type); rec != nullptr) {
*out_info = *rec;
return true;
}
}
}
if (m_symbols != nullptr) {
if (const auto* rec = m_symbols->FindByNid(nid, type); rec != nullptr) {
*out_info = *rec;
return true;
}
}
return false;
}
uint64_t RuntimeLinker::ReadFromElf(Program* program, uint64_t vaddr) {
EXIT_IF(program == nullptr);
EXIT_IF(program->base_vaddr == 0 || program->base_size == 0);
@@ -2208,11 +2019,6 @@ void RuntimeLinker::DeleteProgram(Program* p) {
EXIT_IF(
!Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size));
}
if (program->custom_call_plt_vaddr != 0 || program->custom_call_plt_num != 0) {
const auto size = Jit::CallPlt::GetSize(program->custom_call_plt_num);
EXIT_IF(!Libs::LibKernel::Memory::FreeGuestMemory(program->custom_call_plt_vaddr, size));
}
}
void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
@@ -2255,10 +2061,6 @@ void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
GetDynValue(elf, &program->dynamic_info->fini_array_size, DT_FINI_ARRAYSZ);
GetDynValue(elf, &program->dynamic_info->preinit_array_size, DT_PREINIT_ARRAYSZ);
EXIT_NOT_IMPLEMENTED(elf->HasDynValue(DT_OS_PLTGOT) && elf->HasDynValue(DT_PLTGOT));
GetDynPtr(elf, &program->dynamic_info->pltgot_vaddr, DT_OS_PLTGOT);
GetDynPtr(elf, &program->dynamic_info->pltgot_vaddr, DT_PLTGOT);
Elf64_Sxword jmprel_type = 0;
EXIT_NOT_IMPLEMENTED(elf->HasDynValue(DT_OS_PLTREL) && elf->HasDynValue(DT_PLTREL));
GetDynValue(elf, &jmprel_type, DT_OS_PLTREL);
@@ -2281,6 +2083,11 @@ void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
GetDynValue(elf, &program->dynamic_info->rela_table_total_size, DT_RELASZ);
GetDynValue(elf, &program->dynamic_info->rela_table_entry_size, DT_OS_RELAENT);
GetDynValue(elf, &program->dynamic_info->rela_table_entry_size, DT_RELAENT);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->rela_table_total_size % sizeof(Elf64_Rela) != 0 ||
program->dynamic_info->jmprela_table_size % sizeof(Elf64_Rela) != 0);
const auto rela_count = program->dynamic_info->rela_table_total_size / sizeof(Elf64_Rela);
const auto jmprela_count = program->dynamic_info->jmprela_table_size / sizeof(Elf64_Rela);
program->rela_bits.resize((rela_count + jmprela_count + 7u) / 8u);
GetDynValue(elf, &program->dynamic_info->relative_count, DT_RELACOUNT);
@@ -2327,43 +2134,6 @@ void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
DT_OS_EXPORT_LIB_1);
}
static void InstallRelocateHandler(Program* program) {
KYTY_PROFILER_FUNCTION();
uint64_t pltgot_vaddr = program->dynamic_info->pltgot_vaddr + program->base_vaddr;
uint64_t pltgot_size = static_cast<uint64_t>(3) * 8;
void** pltgot = reinterpret_cast<void**>(pltgot_vaddr);
Common::VirtualMemory::Mode old_mode {};
EXIT_IF(!Libs::LibKernel::Memory::ProtectGuestMemory(
pltgot_vaddr, pltgot_size, Common::VirtualMemory::Mode::Write, &old_mode));
pltgot[1] = program;
pltgot[2] = reinterpret_cast<void*>(RelocateHandler);
EXIT_IF(!Libs::LibKernel::Memory::ProtectGuestMemory(pltgot_vaddr, pltgot_size, old_mode));
if (Common::VirtualMemory::IsExecute(old_mode)) {
Common::VirtualMemory::FlushInstructionCache(pltgot_vaddr, pltgot_size);
}
// TODO(): check if this table already generated by compiler (sometimes it is missing)
if (program->custom_call_plt_vaddr == 0) {
program->custom_call_plt_num =
program->dynamic_info->jmprela_table_size / sizeof(Elf64_Rela);
auto size = Jit::CallPlt::GetSize(program->custom_call_plt_num);
program->custom_call_plt_vaddr = Libs::LibKernel::Memory::AllocateRuntimeMemory(
SYSTEM_RESERVED, size, Common::VirtualMemory::Mode::Write, "custom_call_plt");
EXIT_NOT_IMPLEMENTED(program->custom_call_plt_vaddr == 0);
auto* code = new (reinterpret_cast<void*>(program->custom_call_plt_vaddr))
Jit::CallPlt(program->custom_call_plt_num);
code->SetPltGot(pltgot_vaddr);
EXIT_IF(!Libs::LibKernel::Memory::ProtectGuestMemory(program->custom_call_plt_vaddr, size,
Common::VirtualMemory::Mode::Execute));
Common::VirtualMemory::FlushInstructionCache(program->custom_call_plt_vaddr, size);
}
}
void RuntimeLinker::Relocate(Program* program) {
KYTY_PROFILER_FUNCTION();
@@ -2383,46 +2153,23 @@ void RuntimeLinker::Relocate(Program* program) {
EXIT_NOT_IMPLEMENTED(program->dynamic_info->jmprela_table == nullptr);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->rela_table == nullptr);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->symbol_table == nullptr);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->pltgot_vaddr == 0);
InstallRelocateHandler(program);
const auto rela_count = static_cast<uint32_t>(
program->dynamic_info->rela_table_total_size / sizeof(Elf64_Rela));
std::vector<std::string> unresolved;
const bool imports_only = program->relocated;
RelocateRecords(program->dynamic_info->rela_table, program->dynamic_info->rela_table_total_size,
program, false, imports_only, &unresolved);
RelocateRecords(program->dynamic_info->jmprela_table, program->dynamic_info->jmprela_table_size,
program, true, imports_only, &unresolved);
program->relocated = true;
ForEachRelocation(program, [&](Elf64_Rela* r, uint32_t index, bool jmprela) {
const auto bit_index = (jmprela ? rela_count : 0) + index;
if ((program->rela_bits[bit_index >> 3u] & (1u << (bit_index & 7u))) == 0 &&
RelocateRecord(index, r, program, jmprela)) {
program->rela_bits[bit_index >> 3u] |= 1u << (bit_index & 7u);
}
});
if (program->tls.image_vaddr != 0 && program->tls.init_size != 0 &&
program->tls.init_image.empty()) {
const auto* src = reinterpret_cast<const uint8_t*>(program->tls.image_vaddr);
program->tls.init_image.assign(src, src + program->tls.init_size);
}
if (!unresolved.empty()) {
LOGF("--- Stubbed unresolved imports: %zu ---\n", unresolved.size());
for (const auto& symbol: unresolved) {
LOGF("Stubbed: %s\n", symbol.c_str());
}
}
}
Program* RuntimeLinker::FindProgram(const ModuleId& m, const LibraryId& l) {
Common::LockGuard lock(m_mutex);
for (auto* p: m_programs) {
const auto& export_libs = p->dynamic_info->export_libs;
const auto& export_modules = p->dynamic_info->export_modules;
if (std::find(export_libs.begin(), export_libs.end(), l) != export_libs.end() &&
std::find(export_modules.begin(), export_modules.end(), m) != export_modules.end()) {
return p;
}
}
return nullptr;
}
const ModuleId* RuntimeLinker::FindModule(const Program& program, const std::string& id) {
+2 -8
View File
@@ -86,7 +86,6 @@ struct DynamicInfo {
uint64_t init_array_size = 0;
uint64_t fini_array_size = 0;
uint64_t preinit_array_size = 0;
uint64_t pltgot_vaddr = 0;
Elf64_Rela* jmprela_table = nullptr;
uint64_t jmprela_table_size = 0;
@@ -116,6 +115,7 @@ struct Program {
int32_t unique_id = -1;
RuntimeLinker* rt = nullptr;
uint32_t load_count = 0; // Successful sceKernelLoadStartModule calls.
std::filesystem::path file_name;
std::unique_ptr<Elf64> elf;
std::unique_ptr<DynamicInfo> dynamic_info;
@@ -132,10 +132,8 @@ struct Program {
ThreadLocalStorage tls;
bool fail_if_global_not_resolved = true;
bool dbg_print_reloc = false;
bool relocated = false;
std::vector<uint8_t> rela_bits;
uint64_t proc_param_vaddr = 0;
uint64_t custom_call_plt_vaddr = 0;
uint32_t custom_call_plt_num = 0;
};
class RuntimeLinker {
@@ -171,7 +169,6 @@ public:
void Resolve(const std::string& name, SymbolType type, Program* program, SymbolRecord* out_info,
bool* bind_self);
bool ResolveLoadedSymbolByNid(const std::string& nid, SymbolType type, SymbolRecord* out_info);
SymbolDatabase* Symbols() { return m_symbols.get(); }
@@ -194,14 +191,11 @@ private:
static void SetupTlsHandler(Program* program);
void PreloadAdjacentPrograms();
Program* FindProgram(const ModuleId& m, const LibraryId& l);
static const ModuleId* FindModule(const Program& program, const std::string& id);
static const LibraryId* FindLibrary(const Program& program, const std::string& id);
std::vector<Program*> m_programs;
std::unique_ptr<SymbolDatabase> m_symbols;
bool m_relocated = false;
Common::Mutex m_mutex;
application_heap_malloc_func_t m_application_heap_malloc = nullptr;
+4 -2
View File
@@ -65,12 +65,14 @@ const SymbolRecord* SymbolDatabase::Find(const SymbolResolve& s) const {
return &m_symbols[index];
}
const SymbolRecord* SymbolDatabase::FindByNid(const std::string& nid, SymbolType type) const {
const SymbolRecord* SymbolDatabase::FindByNid(const std::string& nid, SymbolType type,
uint64_t vaddr) const {
auto prefix = nid + "[";
auto suffix = fmt::format("[{}]", magic_enum::enum_name(type));
for (const auto& symbol: m_symbols) {
if (symbol.name.starts_with(prefix) && symbol.name.ends_with(suffix)) {
if ((vaddr == 0 || symbol.vaddr == vaddr) && symbol.name.starts_with(prefix) &&
symbol.name.ends_with(suffix)) {
return &symbol;
}
}
+2 -1
View File
@@ -43,7 +43,8 @@ public:
void Add(const SymbolResolve& s, uint64_t vaddr, const std::string& dbg_name);
[[nodiscard]] const SymbolRecord* Find(const SymbolResolve& s) const;
[[nodiscard]] const SymbolRecord* FindByNid(const std::string& nid, SymbolType type) const;
[[nodiscard]] const SymbolRecord* FindByNid(const std::string& nid, SymbolType type,
uint64_t vaddr = 0) const;
[[nodiscard]] const SymbolRecord* FindByName(const std::string& name, SymbolType type) const;
void DbgDump(const std::string& folder, const std::string& file_name);
+65 -9
View File
@@ -8,6 +8,7 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
@@ -16,14 +17,16 @@ namespace {
namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
std::vector<std::vector<uint8_t>> g_output_pcm;
size_t g_capture_bytes = 0;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
void Check(bool value, const char* text) {
if (!value) {
@@ -146,6 +149,17 @@ std::vector<bool> OutputCalls() {
return g_output_blocking;
}
void CaptureOutputPcm(size_t bytes) {
std::lock_guard lock(g_device_mutex);
g_capture_bytes = bytes;
g_output_pcm.clear();
}
std::vector<std::vector<uint8_t>> OutputPcm() {
std::lock_guard lock(g_device_mutex);
return g_output_pcm;
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -327,6 +341,41 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPcmCopiedBeforeScratchBufferReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle first = 0;
AudioOut2::AudioOut2PortHandle second = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &first) == OK,
"first port create failed");
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &second) == OK,
"second port create failed");
std::vector<float> scratch(512 * 2);
std::vector<float> first_pcm(scratch.size(), 0.25f);
std::vector<float> second_pcm(scratch.size(), -0.5f);
std::copy(first_pcm.begin(), first_pcm.end(), scratch.begin());
SetPcm(first, scratch.data());
std::copy(second_pcm.begin(), second_pcm.end(), scratch.begin());
SetPcm(second, scratch.data());
std::fill(scratch.begin(), scratch.end(), 0.0f);
const auto pcm_bytes = scratch.size() * sizeof(float);
CaptureOutputPcm(pcm_bytes);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "shared-buffer push failed");
const auto output = OutputPcm();
Check(output.size() == 2, "shared-buffer push did not output both ports");
Check(std::memcmp(output[0].data(), first_pcm.data(), pcm_bytes) == 0,
"first port lost PCM when scratch buffer was reused");
Check(std::memcmp(output[1].data(), second_pcm.data(), pcm_bytes) == 0,
"second port lost PCM when scratch buffer was reused");
CaptureOutputPcm(0);
AudioOut2::AudioOut2PortDestroy(first);
AudioOut2::AudioOut2PortDestroy(second);
AudioOut2::AudioOut2ContextDestroy(context);
}
} // namespace
namespace Libs::Audio::AudioInternal {
@@ -363,9 +412,15 @@ bool AudioOutHasDevice(int handle) {
g_device_backed_handles.end();
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) {
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
if (g_capture_bytes != 0) {
for (uint32_t i = 0; i < num; i++) {
const auto* bytes = static_cast<const uint8_t*>(params[i].data);
g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes);
}
}
return 0;
}
@@ -389,6 +444,7 @@ int main() {
TestFloat12ChannelPortOutputsPcm();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
}
+263 -7
View File
@@ -88,6 +88,194 @@ void TestStreamingResample() {
"streaming progress does not match consumed source data");
}
void TestAtrac9ResampleAcrossFrames() {
const auto make_voice = [] {
auto f = std::make_unique<Fixture>(24000);
f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f->voice.state = Ngs2VoicePlayState::Playing;
f->voice.decoded_frame[0] = 0.0f;
f->voice.decoded_frame[1] = 0.5f;
f->voice.frame_samples = 2;
f->voice.next_decoded_frame.resize(f->voice.decoded_frame.size());
f->voice.next_decoded_frame[0] = 1.0f;
f->voice.next_decoded_frame[1] = 1.5f;
f->voice.next_frame_samples = 2;
f->voice.accepts_blocks = false;
return f;
};
auto whole = make_voice();
whole->Render(8);
const std::vector<float> expected {0.0f, 0.25f, 0.5f, 0.75f,
1.0f, 1.25f, 1.5f, 1.5f};
Check(whole->voice.samples == expected && whole->voice.decoded_samples == 4 &&
whole->voice.state == Ngs2VoicePlayState::Empty,
"24 kHz ATRAC9 interpolation or source progress failed at frame boundary");
auto split = make_voice();
split->Render(3);
const auto first = split->voice.samples;
split->Render(5);
std::vector<float> joined = first;
joined.insert(joined.end(), split->voice.samples.begin(), split->voice.samples.end());
Check(joined == expected && split->voice.decoded_samples == 4,
"ATRAC9 resampling changed across render grains");
auto pitched = make_voice();
struct Pitch {
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
float ratio = 2.0f;
uint32_t reserved = 0;
} pitch;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pitched->voice), &pitch.header) == OK,
"ATRAC9 pitch control failed");
pitched->Render(4);
Check(pitched->voice.samples == std::vector<float> {0.0f, 0.5f, 1.0f, 1.5f} &&
pitched->voice.decoded_samples == 4,
"ATRAC9 pitch did not use source-rate phase advancement");
}
void TestAtrac9ResampleAfterStarvation() {
Fixture f(24000);
f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f.voice.state = Ngs2VoicePlayState::Playing;
f.voice.accepts_blocks = true;
f.voice.decoded_frame[0] = 0.0f;
f.voice.decoded_frame[1] = 0.5f;
f.voice.frame_samples = 2;
f.Render(4);
Check(f.voice.samples == std::vector<float> {0.0f, 0.25f, 0.5f, 0.5f} &&
f.voice.decoded_samples == 2,
"ATRAC9 resampling lost the final sample before starvation");
f.Render(2);
Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Playing &&
f.voice.decoded_samples == 2,
"starved ATRAC9 stream consumed source samples");
f.voice.next_decoded_frame.resize(f.voice.decoded_frame.size());
f.voice.next_decoded_frame[0] = 1.0f;
f.voice.next_decoded_frame[1] = 1.5f;
f.voice.next_frame_samples = 2;
f.Render(4);
Check(f.voice.samples == std::vector<float> {1.0f, 1.25f, 1.5f, 1.5f} &&
f.voice.decoded_samples == 4,
"ATRAC9 refill lost source-rate phase");
}
void TestWaveformBlockGeometry() {
const Ngs2WaveformFormat format {NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0};
Ngs2WaveformBlock block {};
Check(Ngs2CalcWaveformBlock(&format, 0, 34534, &block) == OK &&
block.data_offset == 0 && block.data_size == 13056 &&
block.num_skip_samples == 0 && block.num_samples == 34534,
"ATRAC9 block calculation omitted encoded superframes");
Check(Ngs2CalcWaveformBlock(&format, 513, 1, &block) == OK &&
block.data_offset == 192 && block.data_size == 192 &&
block.num_skip_samples == 1 && block.num_samples == 1,
"ATRAC9 block calculation lost the source offset");
Check(Ngs2CalcWaveformBlock(&format, 0, 0, &block) == OK && block.data_size == 0 &&
block.num_samples == 0,
"empty ATRAC9 block acquired encoded data");
Fixture f(24000);
const int16_t silence = 0;
f.Queue(&silence, 0, 0, UINT32_MAX);
f.Render(1);
Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty,
"empty repeated block kept the sampler active");
}
void TestSubmixerSetupAndRouting() {
Fixture f(48000, 2);
Ngs2RackInternal submixer_rack {};
submixer_rack.ngs = &f.system;
submixer_rack.type = Ngs2RackType::Submixer;
submixer_rack.option.submixer.max_channels = 8;
Ngs2VoiceInternal submixer;
submixer.rack = &submixer_rack;
submixer.ports.resize(1);
submixer.matrices.resize(1);
Ngs2RackInternal mastering_rack {};
mastering_rack.type = Ngs2RackType::Mastering;
Ngs2VoiceInternal mastering;
mastering.rack = &mastering_rack;
mastering.channels = 2;
mastering.state = Ngs2VoicePlayState::Playing;
submixer.state = Ngs2VoicePlayState::Playing;
submixer.ports[0].dest = &mastering;
submixer.matrices[0] = {1.0f};
f.voice.ports.push_back({&submixer, 0, 1.0f, -1});
const int16_t pcm[] = {8192, -8192, 16384, -16384};
f.Queue(pcm, 2, 0);
const std::vector<Ngs2VoiceInternal*> voices {&f.voice, &submixer, &mastering};
Ngs2RenderVoice(mastering, voices, 1);
Check(!submixer.has_samples && !mastering.has_samples && f.voice.decoded_samples == 0,
"unconfigured zero-channel submixer claimed to produce samples");
Ngs2SubmixerVoiceSetupParam setup {{16, 0, 0x20000000}, 2, 0};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&submixer), &setup.header) == OK &&
submixer.channels == 2 && submixer.state == Ngs2VoicePlayState::Empty &&
submixer.ports[0].dest == nullptr && submixer.matrices[0].empty(),
"submixer setup did not establish channels and clear old routing");
submixer.state = Ngs2VoicePlayState::Playing;
submixer.ports[0] = {&mastering, 0, 1.0f, 0};
submixer.matrices[0] = {1.0f, 0.0f, 0.0f, 1.0f};
for (float expected: {0.25f, 0.5f}) {
for (auto* voice: voices) {
voice->rendered = false;
}
Ngs2RenderVoice(mastering, voices, 1);
Check(mastering.has_samples && mastering.samples[0] == expected &&
mastering.samples[1] == -expected,
"submixer lost routed samples across render grains");
}
}
void TestPcm16RenderOutput() {
Ngs2Internal system;
system.option = Ngs2DefaultSystemOption();
system.option.num_grain_samples = 4;
Ngs2RackOptionUnion sampler_option {}, mastering_option {};
Ngs2FillDefaultRackOption(0x1000, &sampler_option);
Ngs2FillDefaultRackOption(0x3000, &mastering_option);
sampler_option.common.max_voices = 2;
sampler_option.common.max_ports = 1;
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
std::byte sampler_storage[sizeof(Ngs2RackInternal) + 2 * sizeof(Ngs2VoiceInternal)];
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
std::byte mastering_storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)];
Ngs2ContextBufferInfo sampler_buffer {sampler_storage, sizeof(sampler_storage)};
Ngs2ContextBufferInfo mastering_buffer {mastering_storage, sizeof(mastering_storage)};
uintptr_t sampler_handle = 0, mastering_handle = 0;
const auto system_handle = reinterpret_cast<uintptr_t>(&system);
Check(Ngs2RackCreate(system_handle, 0x1000, &sampler_option.common, &sampler_buffer,
&sampler_handle) == OK &&
Ngs2RackCreate(system_handle, 0x3000, &mastering_option.common,
&mastering_buffer, &mastering_handle) == OK,
"PCM16 render rack creation failed");
auto* samplers = reinterpret_cast<Ngs2VoiceInternal*>(
reinterpret_cast<Ngs2RackInternal*>(sampler_handle) + 1);
auto& mastering = *reinterpret_cast<Ngs2VoiceInternal*>(
reinterpret_cast<Ngs2RackInternal*>(mastering_handle) + 1);
mastering.channels = 1;
mastering.state = Ngs2VoicePlayState::Playing;
const int16_t source[2][4] = {{16384, -16384, 32767, -32768},
{8192, -8192, 32767, -32768}};
for (uint32_t i = 0; i < 2; ++i) {
samplers[i].SetupSampler({0x12, 1, 48000});
samplers[i].blocks.push_back(
{reinterpret_cast<const uint8_t*>(source[i]), {0, sizeof(source[i]), 0, 0, 4}});
samplers[i].ports[0].dest = &mastering;
samplers[i].SetEvent(1);
}
int16_t output_samples[4] = {};
Ngs2RenderBufferInfo output {output_samples, sizeof(output_samples), 0x12, 1};
Check(Ngs2SystemRender(system_handle, &output, 1) == OK &&
output_samples[0] == 24576 && output_samples[1] == -24576 &&
output_samples[2] == 32767 && output_samples[3] == -32768,
"PCM16 output failed to mix or saturate mastering voices");
Check(Ngs2SystemRender(system_handle, &output, 1) == OK &&
std::ranges::all_of(output_samples, [](int16_t sample) { return sample == 0; }),
"PCM16 output retained samples from the previous render");
Check(Ngs2RackDestroy(mastering_handle, nullptr) == OK &&
Ngs2RackDestroy(sampler_handle, nullptr) == OK,
"PCM16 render rack cleanup failed");
}
void TestPitchLoopAndSkip() {
Fixture f(24000);
const int16_t pcm[] = {-30000, 8192, 16384};
@@ -106,6 +294,22 @@ void TestPitchLoopAndSkip() {
}
}
void TestPitchClamps() {
Fixture f(48000);
const int16_t pcm[] = {8192, 0, 0, 0, -8192};
f.Queue(pcm, 5, 0);
struct Pitch {
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
float ratio = 5.0f;
uint32_t reserved = 0;
} pitch;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &pitch.header) == OK,
"out-of-range pitch was rejected");
f.Render(2);
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.25f,
"pitch was not clamped to four times playback speed");
}
void TestStarvationAndPause() {
Fixture f(48000);
const int16_t pcm[] = {16384, 8192};
@@ -149,6 +353,27 @@ void TestMonoRateAndRouting() {
"22.05 kHz effect duration was incorrect");
}
void TestMatrixOutputChannels() {
Fixture f(48000, 2);
Ngs2RackInternal mastering_rack {};
mastering_rack.type = Ngs2RackType::Mastering;
Ngs2VoiceInternal mastering;
mastering.rack = &mastering_rack;
mastering.channels = 8;
mastering.state = Ngs2VoicePlayState::Playing;
f.voice.matrices = {{1.0f, 0.0f, 0.0f, 1.0f}};
f.voice.ports.push_back({&mastering, 0, 1.0f, 0});
const int16_t pcm[] = {8192, -8192};
f.Queue(pcm, 1, 0);
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 1);
Check(mastering.samples[0] == 0.25f && mastering.samples[1] == -0.25f,
"stereo matrix did not route its output channels");
for (size_t channel = 2; channel < mastering.channels; ++channel) {
Check(mastering.samples[channel] == 0.0f,
"matrix routed audio to an unspecified output channel");
}
}
void TestCustomPcmStillPlays() {
Fixture f(48000, 1, Ngs2RackType::CustomSampler);
const int16_t pcm[] = {8192, -16384};
@@ -190,6 +415,29 @@ void TestSamplerReuseResetsRouting() {
}
}
void TestDeferredSamplerSetup() {
Fixture f(48000);
struct Setup {
Ngs2VoiceParamHeader header {40, 0, 0x10000000};
Ngs2WaveformFormat format;
uint32_t flags = 0, reserved = 0;
} setup;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &setup.header) == OK,
"deferred stream setup failed");
Check(f.voice.state == Ngs2VoicePlayState::Empty && f.voice.channels == 0 &&
f.voice.sample_rate == 0 && f.voice.decoder == nullptr,
"deferred stream setup did not clear the sampler");
setup.format = {0x12, 1, 48000};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &setup.header) == OK,
"sampler setup after deferred stream failed");
const int16_t pcm = 8192;
f.Queue(&pcm, 1, 0);
f.voice.SetEvent(1);
f.Render(1);
Check(f.voice.samples[0] == 0.25f,
"sampler did not play after deferred stream setup");
}
void TestPlayStateBeforeRender() {
Fixture f(48000);
const int16_t pcm[] = {8192, -16384};
@@ -270,7 +518,7 @@ void TestStopThenPlayBeforeRender() {
void SetLowPass(Fixture& f, float frequency = 1000.0f, uint64_t mask = 0) {
struct Param {
Ngs2VoiceParamHeader header {56, 0, 0x1000000a};
Ngs2SamplerFilterParam filter {0, 1, 0, 1, 1000.0f, 0.70710678f, 1.0f, {}};
Ngs2SamplerFilterParam filter {0, 1, 1, 0, 1000.0f, 0.70710678f, 1.0f, {}};
} param;
param.filter.frequency = frequency;
param.filter.channel_mask = mask;
@@ -366,8 +614,8 @@ void TestFilterControlLayout() {
Fixture f(48000, 2);
f.rack.option.sampler.max_filters = 8;
// A 56-byte control command with nonzero trailing padding.
alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 0, 0, 1,
0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0, 0x13f};
alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 1, 0, 0,
0, 0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0x13f};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice),
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
Check(f.voice.filters.size() == 1 && f.voice.filters[0].enabled,
@@ -377,8 +625,8 @@ void TestFilterControlLayout() {
const double expected = (1 - std::cos(omega)) / (2 * (1 + std::sin(omega) / (2 * q)));
Check(std::abs(f.voice.filters[0].b0 - expected) < 1e-10,
"FCQ fields were read at the wrong offsets");
words[4] = 1; // Bypass channel 0 in the low half of the 64-bit mask.
words[5] = 1; // Bypass channel 32 in the high half.
words[6] = 1; // Bypass channel 0 in the low half of the 64-bit mask.
words[7] = 1; // Bypass channel 32 in the high half.
Fixture wide(48000, 33);
wide.rack.option.sampler.max_filters = 8;
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
@@ -389,8 +637,8 @@ void TestFilterControlLayout() {
Check(wide.voice.samples[0] == 0.5f && wide.voice.samples[32] == 0.5f &&
std::abs(wide.voice.samples[1] - expected * 0.5) < 1e-7,
"channel mask was not decoded as a 64-bit bypass mask");
words[6] = 0; // OFF ignores unused FCQ values and clears the old delay line.
words[7] = 0x7fc00000;
words[4] = 0; // OFF ignores unused FCQ values and clears the old delay line.
words[8] = 0x7fc00000;
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
Check(!wide.voice.filters[0].enabled && wide.voice.filters[0].history.empty() &&
@@ -664,8 +912,14 @@ void TestFiniteFilterTail() {
} // namespace
int main() {
TestPcm16RenderOutput();
TestSubmixerSetupAndRouting();
TestWaveformBlockGeometry();
TestAtrac9ResampleAfterStarvation();
TestAtrac9ResampleAcrossFrames();
TestResamplingPhaseAfterStarvation();
TestSamplerReuseResetsRouting();
TestDeferredSamplerSetup();
TestLoopCallbacksAndExit();
TestWaveformReadAddress();
TestFilterTailDuringStarvation();
@@ -679,8 +933,10 @@ int main() {
TestCallbackPauseDuringLargeStep();
TestStreamingResample();
TestPitchLoopAndSkip();
TestPitchClamps();
TestStarvationAndPause();
TestMonoRateAndRouting();
TestMatrixOutputChannels();
TestCustomPcmStillPlays();
TestPlayStateBeforeRender();
TestStatePublication();
+25
View File
@@ -2815,6 +2815,31 @@ void TestGraphicsPushConstantLayout() {
}
void TestResourceLimitIsTransactional() {
Fixture accepted;
MemoryInfo accepted_memory;
accepted_memory.kind = ResourceKind::Buffer;
for (uint32_t index = 0; index < ShaderInfo::MaxBuffers; index++) {
const auto handle = accepted.Buffer(
{Value(index), Value(index + 1u), Value(index + 2u), Value(index + 3u)},
index * 4u);
accepted.Emit(ValueOpcode::LoadBufferU32,
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
accepted.AddMemory(accepted_memory, index * 4u));
}
accepted.PlanAndTrack();
Check(accepted.program.info.buffers.size() == 64u &&
accepted.program.descriptor_sources.size() == 64u &&
accepted.program.memory_info.back().resource == 63u,
"compute shader did not retain all 64 distinct buffers");
ShaderComputeInputInfo compute{};
CollectShaderInfo(accepted.program, {.compute = &compute});
AllocateBindings(accepted.program);
const auto *binding = FindBinding(accepted.program.bindings,
DescriptorBindingKind::Buffers);
Check(binding != nullptr && binding->resources.size() == 64u &&
accepted.program.bindings.memory_offset_count == 64u,
"compute shader binding layout truncated the 64 buffers");
Fixture fixture;
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
+33
View File
@@ -18937,6 +18937,38 @@ TestCase Vop1SdwaNotPreservesHighWordDestination() {
return test;
}
TestCase Vop1SdwaNotPartialSourcesAndDestinations() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendBufferLoadDword(&code, 8, 30);
AppendVMovLiteral(&code, 28, 0xabcd5555u);
code.push_back(EncodeVop1(0x37, 28, 249));
code.push_back(EncodeVop1Sdwa(8, 4, 2, 5)); // v28.word0 = ~v8.word1, preserve word1
AppendStoreVgpr(&code, 28, 0);
AppendVMovLiteral(&code, 3, 0xa1b2c3d4u);
code.push_back(EncodeVop1(0x37, 3, 249));
code.push_back(EncodeVop1Sdwa(8, 2, 2, 0)); // v3.byte2 = ~v8.byte0, preserve others
AppendStoreVgpr(&code, 3, 1);
code.push_back(EncodeVop1(0x37, 4, 249));
code.push_back(EncodeVop1Sdwa(8, 0, 0, 0)); // v4.byte0 = ~v8.byte0, zero others
AppendStoreVgpr(&code, 4, 2);
AppendEnd(&code);
TestCase test;
test.name = "Vop1SdwaNotPartialSourcesAndDestinations";
test.code = std::move(code);
test.initial = {0x12345678u, 0u, 0u};
test.expected = {0xabcdedcbu, 0xa187c3d4u, 0x00000087u};
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_MOV_B32, O::V_NOT_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"V_NOT_B32", 3u}};
test.required_spirv = {"OpBitFieldUExtract", "OpNot"};
return test;
}
TestCase Vop1SdwaMovByteDestinations() {
using O = ShaderOpcode;
@@ -28641,6 +28673,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop1SdwaFfblCapturedHighWordSource);
AddCase(Vop1SdwaNotCapturedByte0Source);
AddCase(Vop1SdwaNotPreservesHighWordDestination);
AddCase(Vop1SdwaNotPartialSourcesAndDestinations);
AddCase(Vop1SdwaMovByteDestinations);
AddCase(Vop2SdwaSubNcExactByte2Destination);
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
-2
View File
@@ -3523,8 +3523,6 @@ void TestNewShaderRecompilerVop1SdwaNotDestination() {
check_rejected(0x00260400u, "V_NOT_B32 SDWA accepted source absolute");
check_rejected(0x00062400u, "V_NOT_B32 SDWA accepted clamp");
check_rejected(0x00064400u, "V_NOT_B32 SDWA accepted output modifier");
check_rejected(0x00000400u,
"V_NOT_B32 SDWA partial destination accepted a byte source");
}
void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {