Compare commits

...

6 Commits

9 changed files with 742 additions and 562 deletions
-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)) |
+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);
+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();