mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 13:26:52 +01:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0b682e4b2 | |||
| 16b83a034c | |||
| cf268af839 | |||
| fd2e15ee73 | |||
| 4e8d8c849d | |||
| 1e438136fa | |||
| 313b60e922 | |||
| cda236dbb3 | |||
| 1216fa36cf | |||
| 497166009c | |||
| 0fb3ac5605 | |||
| e056d55dd5 | |||
| 1371219f5c | |||
| cf423df624 | |||
| 5ce4f08364 |
@@ -112,12 +112,15 @@ jobs:
|
||||
- name: Build
|
||||
shell: cmd
|
||||
run: |
|
||||
cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests --parallel
|
||||
cmake --build _Build/windows --target launcher ^
|
||||
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests --parallel
|
||||
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|virtual_memory_allocation)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -198,14 +201,16 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/macos \
|
||||
--target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
|
||||
--target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|ime_dialog|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -379,14 +384,16 @@ jobs:
|
||||
run: |
|
||||
cmake --build _Build/linux \
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
|
||||
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|ime_dialog|page_manager|memory_tracker|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
|
||||
@@ -411,6 +411,11 @@ add_executable(bit_array_tests EXCLUDE_FROM_ALL
|
||||
)
|
||||
target_include_directories(bit_array_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(lru_cache_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/LruCacheTests.cpp"
|
||||
)
|
||||
target_include_directories(lru_cache_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(ime_dialog_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/ImeDialogTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/libs/imeCommon.cpp"
|
||||
@@ -555,6 +560,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
|
||||
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
|
||||
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
|
||||
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
|
||||
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
|
||||
add_test(NAME resource_materialization COMMAND $<TARGET_FILE:resource_materialization_tests>)
|
||||
add_test(NAME resource_tracking COMMAND $<TARGET_FILE:resource_tracking_tests>)
|
||||
@@ -619,6 +625,7 @@ if(BUILD_TESTING)
|
||||
memory_tracker_tests
|
||||
page_manager_tests
|
||||
bit_array_tests
|
||||
lru_cache_tests
|
||||
shader_vertex_metadata_tests
|
||||
resource_materialization_tests
|
||||
resource_tracking_tests
|
||||
@@ -629,6 +636,7 @@ if(BUILD_TESTING)
|
||||
virtual_memory_allocation_tests
|
||||
audio_out2_port_tests
|
||||
ngs2_sampler_tests
|
||||
save_data_memory_tests
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ struct GraphicContext {
|
||||
bool memory_budget_ext_enabled = false;
|
||||
bool compute_subgroup_size_control_enabled = false;
|
||||
bool sample_rate_shading_enabled = false;
|
||||
// bool fp64_denorm_preserve = false; // Temporarily disabled.
|
||||
bool attachment_feedback_loop_enabled = false;
|
||||
bool provoking_vertex_last_enabled = false;
|
||||
bool supports_block_texel_view = false;
|
||||
|
||||
@@ -335,8 +335,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
|
||||
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
|
||||
resource.written && !resource.read && !resource.atomic;
|
||||
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
|
||||
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
|
||||
resource.atomic;
|
||||
const bool format_ok =
|
||||
raw_sint_storage ||
|
||||
raw_sint_storage || raw_float_atomic ||
|
||||
(numeric_class != Prospero::TextureNumericClass::Unsupported &&
|
||||
numeric_class != Prospero::TextureNumericClass::Sint &&
|
||||
uint_resource == (numeric_class == Prospero::TextureNumericClass::Uint) &&
|
||||
@@ -640,7 +642,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
pixel_format = depth_format->depth_attachment_format;
|
||||
}
|
||||
}
|
||||
const auto storage_view_format = storage && format == Prospero::BufferFormat::k32SInt
|
||||
const auto storage_view_format = storage && (resource.atomic ||
|
||||
format == Prospero::BufferFormat::k32SInt)
|
||||
? vk::Format::eR32Uint
|
||||
: SrgbStorageViewFormat(pixel_format);
|
||||
const auto view_format = storage && storage_view_format != vk::Format::eUndefined
|
||||
|
||||
@@ -564,8 +564,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
vk::PhysicalDeviceVulkan11Properties properties11 {};
|
||||
properties11.pNext = &subgroup_size_control;
|
||||
|
||||
vk::PhysicalDeviceFloatControlsProperties float_controls {};
|
||||
float_controls.pNext = &properties11;
|
||||
vk::PhysicalDeviceProperties2 properties2 {};
|
||||
properties2.pNext = &properties11;
|
||||
properties2.pNext = &float_controls;
|
||||
|
||||
if (graphics.mesh_shader_enabled) {
|
||||
subgroup_size_control.pNext = &graphics.mesh_shader_properties;
|
||||
@@ -628,6 +630,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
device_features.vertexPipelineStoresAndAtomics = VK_TRUE;
|
||||
graphics.sample_rate_shading_enabled = true;
|
||||
device_features.shaderInt64 = VK_TRUE;
|
||||
device_features.shaderFloat64 =
|
||||
supported_features2.features.shaderFloat64 &&
|
||||
float_controls.shaderSignedZeroInfNanPreserveFloat64 &&
|
||||
float_controls.shaderRoundingModeRTEFloat32;
|
||||
// if (device_features.shaderFloat64 && !float_controls.shaderDenormPreserveFloat64) {
|
||||
// Log::WriteToConsoleAndLog(
|
||||
// "WARNING: Vulkan device does not guarantee FP64 denormal preservation; "
|
||||
// "continuing with native FP64 arithmetic. Very small values may be flushed to zero.\n");
|
||||
// }
|
||||
|
||||
vk::PhysicalDeviceRobustness2FeaturesEXT robustness2 {};
|
||||
#if defined(__APPLE__)
|
||||
|
||||
@@ -198,6 +198,7 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
SpirvRequirements requirements {};
|
||||
for (const auto* block: program.blocks) {
|
||||
for (const auto& inst: *block) {
|
||||
requirements.float64 |= inst.GetType() == IR::Type::F64;
|
||||
if (IR::BufferAccessOf(inst.GetOpcode()) == IR::BufferAccess::Atomic &&
|
||||
inst.GetType() == IR::Type::U64) {
|
||||
requirements.buffer_int64_atomics = true;
|
||||
|
||||
@@ -291,9 +291,20 @@ uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitF32ToU32(state, arg0, false);
|
||||
}
|
||||
|
||||
uint32_t EmitConvertF32S32(EmitterState& state, uint32_t arg0) {
|
||||
const auto signed_value = Unary(state, spv::OpBitcast, TypeI32(state), arg0);
|
||||
return EmitNative<spv::OpConvertSToF, IR::Type::F32>(state, signed_value);
|
||||
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0) {
|
||||
const auto converted = Unary(state, spv::OpFConvert, TypeF32(state), arg0);
|
||||
const auto source = EmitNative<spv::OpCompositeExtract, IR::Type::U32>(
|
||||
state, Unary(state, spv::OpBitcast, TypeU64(state), arg0), 1u);
|
||||
const auto exponent = EmitAndConstant(state, source, 0x7ff00000u);
|
||||
const auto overflow =
|
||||
Binary(state, spv::OpLogicalAnd, TypeBool(state),
|
||||
EmitCompareU32Constant(state, spv::OpUGreaterThan, exponent, 0x47e00000u),
|
||||
EmitCompareU32Constant(state, spv::OpULessThan, exponent, 0x7ff00000u));
|
||||
const auto clamped = EmitOrU32(state, EmitAndConstant(state, source, 0x80000000u),
|
||||
ConstantU32(state, 0x7f7fffffu));
|
||||
return EmitFlushF32DenormToSignedZero(
|
||||
state, Select(state, TypeF32(state), overflow,
|
||||
Unary(state, spv::OpBitcast, TypeF32(state), clamped), converted));
|
||||
}
|
||||
|
||||
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1) {
|
||||
@@ -453,6 +464,11 @@ uint32_t EmitFPRecip32(EmitterState& state, uint32_t arg0) {
|
||||
return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), source);
|
||||
}
|
||||
|
||||
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0) {
|
||||
const auto one = state.builder.Constant(spv::OpConstant, TypeF64(state), 0u, 0x3ff00000u);
|
||||
return EmitNative<spv::OpFDiv, IR::Type::F64>(state, one, arg0);
|
||||
}
|
||||
|
||||
uint32_t EmitFPRecipIFlag32(EmitterState& state, uint32_t arg0) {
|
||||
// Integer-to-float inputs used by IFLAG cannot be denormal.
|
||||
return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0);
|
||||
|
||||
@@ -877,7 +877,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return;
|
||||
}
|
||||
const auto atomic_opcode = ImageAtomicOpcode(op);
|
||||
if (atomic_opcode != spv::OpNop) {
|
||||
if (image_info.access == IR::ImageAccess::Atomic) {
|
||||
const auto dimension = image.dimension;
|
||||
ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 3), [&]() {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
@@ -887,6 +887,15 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
StorageImageDescriptorPointer(state, mem.resource),
|
||||
CoordU32(ctx, mem, *address, dimension),
|
||||
ConstantU32(state, 0));
|
||||
if (op == IR::ValueOpcode::ImageAtomicFMin32 ||
|
||||
op == IR::ValueOpcode::ImageAtomicFMax32) {
|
||||
return AtomicUpdate(state, pointer, IR::ResourceKind::Image,
|
||||
[&](uint32_t old) {
|
||||
return EmitFloatAtomicReplacement(
|
||||
state, old, ctx.Arg(inst, 2),
|
||||
op == IR::ValueOpcode::ImageAtomicFMax32);
|
||||
});
|
||||
}
|
||||
const auto old = state.builder.AllocateId();
|
||||
state.builder.AddFunction(atomic_opcode, TypeU32(state), old, pointer,
|
||||
ConstantU32(state, spv::ScopeDevice),
|
||||
|
||||
@@ -13,13 +13,22 @@ inline constexpr auto EmitConvertU32U16 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitConvertU32U8 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitBitCastU32F32 = EmitBitCastU16F16;
|
||||
EMIT_NATIVE(BitCastF32U32, OpBitcast, F32, uint32_t)
|
||||
EMIT_NATIVE(BitCastU64F64, OpBitcast, U64, uint32_t)
|
||||
EMIT_NATIVE(BitCastF64U64, OpBitcast, F64, uint32_t)
|
||||
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0);
|
||||
uint32_t EmitConvertU8U32(EmitterState& state, uint32_t arg0);
|
||||
uint32_t EmitConvertF16F32(EmitterState& state, uint32_t arg0);
|
||||
inline constexpr auto EmitConvertF32F16 = EmitF16BitsToF32;
|
||||
uint32_t EmitConvertS32F32(EmitterState& state, uint32_t arg0);
|
||||
uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0);
|
||||
uint32_t EmitConvertF32S32(EmitterState& state, uint32_t arg0);
|
||||
template <IR::Type type>
|
||||
uint32_t EmitConvertSigned32ToFloat(EmitterState& state, uint32_t arg0) {
|
||||
const auto signed_value = Unary(state, spv::OpBitcast, TypeI32(state), arg0);
|
||||
return EmitNative<spv::OpConvertSToF, type>(state, signed_value);
|
||||
}
|
||||
inline constexpr auto EmitConvertF32S32 = EmitConvertSigned32ToFloat<IR::Type::F32>;
|
||||
inline constexpr auto EmitConvertF64S32 = EmitConvertSigned32ToFloat<IR::Type::F64>;
|
||||
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0);
|
||||
EMIT_NATIVE(ConvertF32U32, OpConvertUToF, F32, uint32_t)
|
||||
EMIT_NATIVE(CompositeConstructU64, OpCompositeConstruct, U64, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(CompositeConstructU32x2, OpCompositeConstruct, U32x2, uint32_t, uint32_t)
|
||||
@@ -120,6 +129,10 @@ inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
|
||||
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPSub32, OpFSub, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPMul32, OpFMul, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPMul64, OpFMul, F64, uint32_t, uint32_t)
|
||||
inline constexpr auto EmitFPFma64 =
|
||||
EmitGlsl<GLSLstd450Fma, IR::Type::F64, uint32_t, uint32_t, uint32_t>;
|
||||
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0);
|
||||
inline constexpr auto EmitFPFma32 =
|
||||
EmitGlsl<GLSLstd450Fma, IR::Type::F32, uint32_t, uint32_t, uint32_t>;
|
||||
uint32_t EmitFPMin32(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
@@ -271,6 +284,8 @@ inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicXor32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicFMin32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicFMax32 = EmitImage;
|
||||
void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
inline constexpr auto EmitPhi = EmitUnreachable;
|
||||
inline constexpr auto EmitTessellationBase = EmitUnreachable;
|
||||
|
||||
@@ -68,6 +68,7 @@ struct SpirvRequirements {
|
||||
bool pixel_valid_mask = false;
|
||||
bool buffer_int64_atomics = false;
|
||||
bool coherent_buffers = false;
|
||||
bool float64 = false;
|
||||
};
|
||||
|
||||
SpirvRequirements AnalyzeProgramRequirements(const IR::Program& program);
|
||||
@@ -139,6 +140,7 @@ uint32_t TypeU32Pair(EmitterState& state);
|
||||
uint32_t TypeI32(EmitterState& state);
|
||||
uint32_t TypeI32Pair(EmitterState& state);
|
||||
uint32_t TypeF32(EmitterState& state);
|
||||
uint32_t TypeF64(EmitterState& state);
|
||||
uint32_t TypeU32Vector(EmitterState& state, uint32_t components);
|
||||
|
||||
uint32_t TypeU32Composite(EmitterState& state, uint32_t components);
|
||||
@@ -167,6 +169,10 @@ template <spv::Op opcode, IR::Type type, typename... Args>
|
||||
uint32_t EmitNative(EmitterState& state, Args... args) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(opcode, TypeId(state, type), result, args...);
|
||||
if constexpr (type == IR::Type::F64 &&
|
||||
(opcode == spv::OpFMul || opcode == spv::OpFDiv || opcode == spv::OpExtInst)) {
|
||||
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ uint32_t TypeF32(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeFloat, 32);
|
||||
}
|
||||
|
||||
uint32_t TypeF64(EmitterState& state) {
|
||||
return state.builder.Type(spv::OpTypeFloat, 64);
|
||||
}
|
||||
|
||||
uint32_t TypeU32Vector(EmitterState& state, uint32_t components) {
|
||||
return state.builder.Type(spv::OpTypeVector, TypeU32(state), components);
|
||||
}
|
||||
@@ -679,6 +683,19 @@ void DefineModule(EmitterState& state) {
|
||||
// contract prevents host compilers from treating synthesized IEEE values as finite.
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
|
||||
32u);
|
||||
if (state.requirements.float64) {
|
||||
EXIT_NOT_IMPLEMENTED(state.program.stage != ShaderType::Compute);
|
||||
EXIT_NOT_IMPLEMENTED(state.input_info.compute->float_mode != 0xc0);
|
||||
// MODE=0xc0 uses round-to-nearest-even and preserves FP64 input/output denormals.
|
||||
state.builder.RequireCapability(spv::CapabilityFloat64);
|
||||
state.builder.RequireCapability(spv::CapabilityRoundingModeRTE);
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
|
||||
64u);
|
||||
// FP64 denormal preservation is temporarily disabled.
|
||||
// state.builder.RequireCapability(spv::CapabilityDenormPreserve);
|
||||
// state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeDenormPreserve, 64u);
|
||||
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeRoundingModeRTE, 32u);
|
||||
}
|
||||
if (const auto* cs = ShaderWorkgroupInput(state.program.stage, state.input_info)) {
|
||||
uint32_t local_x = state.requirements.compute_derivatives ? 2u : 1u;
|
||||
uint32_t local_y = state.requirements.compute_derivatives ? 2u : 1u;
|
||||
|
||||
@@ -440,6 +440,7 @@ uint32_t TypeId(EmitterState& state, IR::Type type) {
|
||||
case IR::Type::U64: return TypeU64(state);
|
||||
case IR::Type::U32x2: return TypeU32Pair(state);
|
||||
case IR::Type::F32: return TypeF32(state);
|
||||
case IR::Type::F64: return TypeF64(state);
|
||||
case IR::Type::U32x3: return TypeU32Vector(state, 3);
|
||||
case IR::Type::U32x4: return TypeU32Vector(state, 4);
|
||||
case IR::Type::F32x2: return TypeF32Vector(state, 2);
|
||||
|
||||
@@ -198,6 +198,8 @@ constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = {
|
||||
{0x18u, Opcode::IMAGE_ATOMIC_AND},
|
||||
{0x19u, Opcode::IMAGE_ATOMIC_OR},
|
||||
{0x1au, Opcode::IMAGE_ATOMIC_XOR},
|
||||
{0x1eu, Opcode::IMAGE_ATOMIC_FMIN},
|
||||
{0x1fu, Opcode::IMAGE_ATOMIC_FMAX},
|
||||
};
|
||||
|
||||
constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCODE_LIST);
|
||||
|
||||
@@ -579,6 +579,8 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::IMAGE_ATOMIC_AND:
|
||||
case Opcode::IMAGE_ATOMIC_OR:
|
||||
case Opcode::IMAGE_ATOMIC_XOR:
|
||||
case Opcode::IMAGE_ATOMIC_FMIN:
|
||||
case Opcode::IMAGE_ATOMIC_FMAX:
|
||||
case Opcode::IMAGE_LOAD:
|
||||
case Opcode::IMAGE_LOAD_MIP:
|
||||
case Opcode::IMAGE_GET_RESINFO:
|
||||
|
||||
@@ -154,6 +154,8 @@ enum class Opcode {
|
||||
V_CUBEMA_F32,
|
||||
V_CNDMASK_B32,
|
||||
V_DOT2C_F32_F16,
|
||||
V_CVT_F64_I32,
|
||||
V_CVT_F32_F64,
|
||||
V_CVT_F32_I32,
|
||||
V_CVT_F32_U32,
|
||||
V_CVT_U32_F32,
|
||||
@@ -173,6 +175,7 @@ enum class Opcode {
|
||||
V_CVT_F32_UBYTE1,
|
||||
V_CVT_F32_UBYTE2,
|
||||
V_CVT_F32_UBYTE3,
|
||||
V_RCP_F64,
|
||||
V_RCP_F32,
|
||||
V_RCP_IFLAG_F32,
|
||||
V_FRACT_F32,
|
||||
@@ -234,6 +237,8 @@ enum class Opcode {
|
||||
V_MAD_I32_I24,
|
||||
V_MAD_U32_U24,
|
||||
V_MAD_U64_U32,
|
||||
V_FMA_F64,
|
||||
V_MUL_F64,
|
||||
V_FMA_F32,
|
||||
V_FMA_F16,
|
||||
V_PACK_B32_F16,
|
||||
@@ -571,6 +576,8 @@ enum class Opcode {
|
||||
IMAGE_ATOMIC_AND,
|
||||
IMAGE_ATOMIC_OR,
|
||||
IMAGE_ATOMIC_XOR,
|
||||
IMAGE_ATOMIC_FMIN,
|
||||
IMAGE_ATOMIC_FMAX,
|
||||
IMAGE_SAMPLE,
|
||||
IMAGE_GATHER4_L,
|
||||
IMAGE_GATHER4_LZ,
|
||||
|
||||
@@ -87,6 +87,9 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
|
||||
{0x00u, Opcode::V_NOP},
|
||||
{0x01u, Opcode::V_MOV_B32},
|
||||
{0x02u, Opcode::V_READFIRSTLANE_B32},
|
||||
{0x04u, Opcode::V_CVT_F64_I32},
|
||||
{0x0fu, Opcode::V_CVT_F32_F64},
|
||||
{0x2fu, Opcode::V_RCP_F64},
|
||||
{0x05u, Opcode::V_CVT_F32_I32},
|
||||
{0x06u, Opcode::V_CVT_F32_U32},
|
||||
{0x07u, Opcode::V_CVT_U32_F32},
|
||||
@@ -146,6 +149,9 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
|
||||
{0x00u, Opcode::V_NOP},
|
||||
{0x01u, Opcode::V_MOV_B32},
|
||||
{0x02u, Opcode::V_READFIRSTLANE_B32},
|
||||
{0x04u, Opcode::V_CVT_F64_I32},
|
||||
{0x0fu, Opcode::V_CVT_F32_F64},
|
||||
{0x2fu, Opcode::V_RCP_F64},
|
||||
{0x05u, Opcode::V_CVT_F32_I32},
|
||||
{0x06u, Opcode::V_CVT_F32_U32},
|
||||
{0x07u, Opcode::V_CVT_U32_F32},
|
||||
@@ -267,6 +273,8 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
|
||||
{0x146u, Opcode::V_CUBETC_F32},
|
||||
{0x147u, Opcode::V_CUBEMA_F32},
|
||||
{0x14bu, Opcode::V_FMA_F32},
|
||||
{0x14cu, Opcode::V_FMA_F64},
|
||||
{0x165u, Opcode::V_MUL_F64},
|
||||
{0x148u, Opcode::V_BFE_U32},
|
||||
{0x149u, Opcode::V_BFE_I32},
|
||||
{0x14au, Opcode::V_BFI_B32},
|
||||
@@ -452,6 +460,8 @@ bool IsVopcCompareExec(Opcode opcode);
|
||||
|
||||
bool IsVop1FloatSourceOpcode(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::V_CVT_F32_F64:
|
||||
case Opcode::V_RCP_F64:
|
||||
case Opcode::V_MOV_B32:
|
||||
case Opcode::V_CVT_F32_F16:
|
||||
case Opcode::V_CVT_U32_F32:
|
||||
@@ -522,8 +532,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(),
|
||||
@@ -707,6 +719,11 @@ void ApplyDppModifier(Operand& operand, uint32_t modifier, uint32_t encoding) {
|
||||
|
||||
void DecodeVop1Dpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
uint32_t opcode, uint32_t vdst, Instruction& inst) {
|
||||
if (inst.opcode == Opcode::V_CVT_F64_I32 || inst.opcode == Opcode::V_CVT_F32_F64 ||
|
||||
inst.opcode == Opcode::V_RCP_F64) {
|
||||
SetUnsupported(inst, Family::VOP1, opcode, "FP64 instructions do not support DPP");
|
||||
return;
|
||||
}
|
||||
const auto modifier = code[word_index + 1u];
|
||||
const auto src0 = modifier & 0xffu;
|
||||
SetRawWords(inst, code, word_index, 2);
|
||||
@@ -756,6 +773,7 @@ bool IsVop2FloatOpcode(Opcode opcode) {
|
||||
|
||||
bool IsVop1FloatResultOpcode(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::V_CVT_F32_F64:
|
||||
case Opcode::V_CVT_F32_I32:
|
||||
case Opcode::V_CVT_F32_U32:
|
||||
case Opcode::V_CVT_F32_F16:
|
||||
@@ -1176,6 +1194,7 @@ void DecodeVopcDpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_in
|
||||
|
||||
uint32_t NativeVop3SourceCount(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Opcode::V_MUL_F64:
|
||||
case Opcode::V_MUL_LO_U32:
|
||||
case Opcode::V_MUL_HI_U32:
|
||||
case Opcode::V_MUL_LO_I32:
|
||||
@@ -1326,6 +1345,8 @@ bool SupportsNativeVop3SourceModifiers(Opcode opcode) {
|
||||
case Opcode::V_MAX_F32:
|
||||
case Opcode::V_MAC_F32:
|
||||
case Opcode::V_MAD_F32:
|
||||
case Opcode::V_MUL_F64:
|
||||
case Opcode::V_FMA_F64:
|
||||
case Opcode::V_FMA_F32:
|
||||
case Opcode::V_PACK_B32_F16:
|
||||
case Opcode::V_CUBEID_F32:
|
||||
|
||||
@@ -223,7 +223,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
|
||||
const bool store_or_atomic = decoded.opcode == Decoder::Opcode::IMAGE_STORE ||
|
||||
decoded.opcode == Decoder::Opcode::IMAGE_STORE_MIP ||
|
||||
(decoded.opcode >= Decoder::Opcode::IMAGE_ATOMIC_SWAP &&
|
||||
decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_XOR);
|
||||
decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_FMAX);
|
||||
if (store_or_atomic) {
|
||||
return index == 0u ? decoded.dst : decoded.src0;
|
||||
}
|
||||
@@ -1026,6 +1026,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_XOR:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_FMIN:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMin32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_FMAX:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMax32);
|
||||
|
||||
case Decoder::Opcode::FLAT_LOAD_UBYTE:
|
||||
case Decoder::Opcode::FLAT_LOAD_SBYTE:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
@@ -225,9 +226,28 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
|
||||
}
|
||||
return ir.INotEqual(ReadRawU32(operand), IR::U32(IR::Value(0u)));
|
||||
}
|
||||
if (type == IR::Type::U64) {
|
||||
const auto pair = ReadU32Pair(operand);
|
||||
return ir.ConstructU64(pair[0], pair[1]);
|
||||
if (type == IR::Type::U64 || type == IR::Type::F64) {
|
||||
auto pair = ReadU32Pair(operand);
|
||||
if (type == IR::Type::F64) {
|
||||
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
|
||||
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
|
||||
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
const auto bits = operand.value == 0x3e22f983u
|
||||
? 0x3fc45f306dc9c882ull
|
||||
: std::bit_cast<uint64_t>(static_cast<double>(
|
||||
std::bit_cast<float>(operand.value)));
|
||||
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
|
||||
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
|
||||
}
|
||||
if (operand.absolute) {
|
||||
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
|
||||
}
|
||||
if (operand.negate) {
|
||||
pair[1] = ir.BitwiseXor(pair[1], IR::U32(IR::Value(0x80000000u)));
|
||||
}
|
||||
}
|
||||
const auto bits = ir.ConstructU64(pair[0], pair[1]);
|
||||
return type == IR::Type::F64 ? ir.Emit(IR::ValueOpcode::BitCastF64U64, {bits}) : IR::Value(bits);
|
||||
}
|
||||
auto bits = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
|
||||
if (TypesOverlap(type, IR::Type::F32) && !TypesOverlap(type, IR::Type::U32)) {
|
||||
@@ -403,6 +423,10 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
|
||||
Write16Bits(operand, IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {bits})));
|
||||
return;
|
||||
}
|
||||
if (type == IR::Type::F64) {
|
||||
value = ir.Emit(IR::ValueOpcode::BitCastU64F64, {value});
|
||||
type = IR::Type::U64;
|
||||
}
|
||||
if (type == IR::Type::U64) {
|
||||
WriteU32Pair(operand, {ir.CompositeExtract(value, 0), ir.CompositeExtract(value, 1)});
|
||||
return;
|
||||
@@ -842,6 +866,14 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
|
||||
include_vector(inst.src1);
|
||||
include_vector(inst.src2);
|
||||
include_vector(inst.src3);
|
||||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::V_CVT_F64_I32: include_vector(inst.dst, 2u); break;
|
||||
case Decoder::Opcode::V_FMA_F64: include_vector(inst.src2, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_MUL_F64: include_vector(inst.src1, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_RCP_F64: include_vector(inst.dst, 2u); [[fallthrough]];
|
||||
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
|
||||
default: break;
|
||||
}
|
||||
if (inst.family == Decoder::Family::DS) {
|
||||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::DS_WRITE_B64:
|
||||
|
||||
@@ -297,6 +297,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CVT_F32_UBYTE3: V_CVT_F32_UBYTE(inst, 3); return;
|
||||
case O::V_CVT_F32_U32: V_CVT_F32_U32(inst); return;
|
||||
case O::V_CVT_F32_I32: V_CVT_F32_I32(inst); return;
|
||||
case O::V_CVT_F64_I32: return FloatUnary(inst, IR::ValueOpcode::ConvertF64S32);
|
||||
case O::V_CVT_F32_F64: return FloatUnary(inst, IR::ValueOpcode::ConvertF32F64);
|
||||
case O::V_CVT_U32_F32: V_CVT_U32_F32(inst); return;
|
||||
case O::V_CVT_I32_F32: V_CVT_I32_F32(inst); return;
|
||||
case O::V_CVT_F16_F32: V_CVT_F16_F32(inst); return;
|
||||
@@ -394,6 +396,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
|
||||
case O::V_FREXP_MANT_F32: return V_FREXP_MANT_F32(inst);
|
||||
case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32);
|
||||
case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64);
|
||||
case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32);
|
||||
case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32);
|
||||
case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32);
|
||||
@@ -410,6 +413,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_SUB_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, false);
|
||||
case O::V_SUBREV_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, true);
|
||||
case O::V_MUL_F32: return FloatBinary(inst, IR::ValueOpcode::FPMul32, false);
|
||||
case O::V_MUL_F64: return FloatBinary(inst, IR::ValueOpcode::FPMul64, false);
|
||||
case O::V_MIN_F32: return FloatBinary(inst, IR::ValueOpcode::FPMin32, false);
|
||||
case O::V_MAX_F32: return FloatBinary(inst, IR::ValueOpcode::FPMax32, false);
|
||||
case O::V_LDEXP_F32: return FloatBinary(inst, IR::ValueOpcode::FPLdexp, false);
|
||||
@@ -418,6 +422,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_MADAK_F32:
|
||||
case O::V_MAD_F32:
|
||||
case O::V_FMA_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, false, true);
|
||||
case O::V_FMA_F64: return FloatTernary(inst, IR::ValueOpcode::FPFma64, false, false);
|
||||
case O::V_MIN3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMinTri32, false, false);
|
||||
case O::V_MAX3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMaxTri32, false, false);
|
||||
case O::V_MED3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMedTri32, false, false);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,6 +30,7 @@ std::string TypeName(Type type) {
|
||||
"ImageResource",
|
||||
"SamplerResource",
|
||||
"ImageAddress",
|
||||
"F64",
|
||||
};
|
||||
const auto bits = static_cast<uint32_t>(type);
|
||||
if (bits == 0) {
|
||||
|
||||
@@ -27,6 +27,7 @@ enum class Type : uint32_t {
|
||||
ImageResource = 1u << 17u,
|
||||
SamplerResource = 1u << 18u,
|
||||
ImageAddress = 1u << 19u,
|
||||
F64 = 1u << 20u,
|
||||
};
|
||||
|
||||
constexpr Type operator|(Type lhs, Type rhs) {
|
||||
|
||||
@@ -33,6 +33,7 @@ constexpr Type U32 = Type::U32;
|
||||
constexpr Type U64 = Type::U64;
|
||||
constexpr Type F16 = Type::F16;
|
||||
constexpr Type F32 = Type::F32;
|
||||
constexpr Type F64 = Type::F64;
|
||||
constexpr Type U32x2 = Type::U32x2;
|
||||
constexpr Type U32x3 = Type::U32x3;
|
||||
constexpr Type U32x4 = Type::U32x4;
|
||||
@@ -220,6 +221,8 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::ImageAtomicAnd32:
|
||||
case ValueOpcode::ImageAtomicOr32:
|
||||
case ValueOpcode::ImageAtomicXor32:
|
||||
case ValueOpcode::ImageAtomicFMin32:
|
||||
case ValueOpcode::ImageAtomicFMax32:
|
||||
return {ImageAccess::Atomic, ImageResourceClass::Storage, false};
|
||||
default: return {};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ VALUE_OPCODE(BitCastU16F16, U16, F16)
|
||||
VALUE_OPCODE(BitCastF16U16, F16, U16)
|
||||
VALUE_OPCODE(BitCastU32F32, U32, F32)
|
||||
VALUE_OPCODE(BitCastF32U32, F32, U32)
|
||||
VALUE_OPCODE(BitCastU64F64, U64, F64)
|
||||
VALUE_OPCODE(BitCastF64U64, F64, U64)
|
||||
VALUE_OPCODE(ConvertU16U32, U16, U32)
|
||||
VALUE_OPCODE(ConvertU32U16, U32, U16)
|
||||
VALUE_OPCODE(ConvertU8U32, U8, U32)
|
||||
@@ -61,6 +63,8 @@ VALUE_OPCODE(ConvertF16F32, F16, F32)
|
||||
VALUE_OPCODE(ConvertS32F32, U32, F32)
|
||||
VALUE_OPCODE(ConvertU32F32, U32, F32)
|
||||
VALUE_OPCODE(ConvertF32S32, F32, U32)
|
||||
VALUE_OPCODE(ConvertF64S32, F64, U32)
|
||||
VALUE_OPCODE(ConvertF32F64, F32, F64)
|
||||
VALUE_OPCODE(ConvertF32U32, F32, U32)
|
||||
VALUE_OPCODE(CompositeConstructU64, U64, U32, U32)
|
||||
VALUE_OPCODE(CompositeConstructU32x2, U32x2, U32, U32)
|
||||
@@ -166,6 +170,9 @@ VALUE_OPCODE(FPAdd32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPSub32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPFma32, F32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMul32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMul64, F64, F64, F64)
|
||||
VALUE_OPCODE(FPFma64, F64, F64, F64, F64)
|
||||
VALUE_OPCODE(FPRecip64, F64, F64)
|
||||
VALUE_OPCODE(FPMin32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMax32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMinTri32, F32, F32, F32, F32)
|
||||
@@ -284,6 +291,8 @@ VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicFMin32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicFMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(GetAttribute, U32, U32, U32)
|
||||
VALUE_OPCODE(GetInterpolationParameter, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(SetAttribute, Void, U32x4, U1)
|
||||
|
||||
@@ -485,7 +485,8 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
image.cube = DescriptorIsCube(descriptor);
|
||||
const auto format =
|
||||
static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
|
||||
if (base.atomic && format != Prospero::BufferFormat::k32UInt) {
|
||||
if (base.atomic && format != Prospero::BufferFormat::k32UInt &&
|
||||
format != Prospero::BufferFormat::k32Float) {
|
||||
return SpecializationFail(
|
||||
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
|
||||
static_cast<uint32_t>(format)));
|
||||
@@ -514,7 +515,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
fmt::format("storage image descriptor {} uses unsupported format {}", i,
|
||||
static_cast<uint32_t>(format)));
|
||||
}
|
||||
if (raw_sint_storage) {
|
||||
if (raw_sint_storage || base.atomic) {
|
||||
image.numeric_class = Prospero::TextureNumericClass::Uint;
|
||||
}
|
||||
} else if (image.numeric_class == Prospero::TextureNumericClass::Unsupported ||
|
||||
|
||||
@@ -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)) |
|
||||
@@ -640,6 +622,7 @@ static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
|
||||
info.threads_num[2] = regs.cs_regs.num_thread_z;
|
||||
info.lds_size_dwords = static_cast<uint32_t>(regs.cs_regs.lds_size) * 128u;
|
||||
info.scratch_size_dwords = data.scratch_size_dwords;
|
||||
info.float_mode = regs.cs_regs.float_mode;
|
||||
info.group_id[0] = regs.cs_regs.tgid_x_en != 0;
|
||||
info.group_id[1] = regs.cs_regs.tgid_y_en != 0;
|
||||
info.group_id[2] = regs.cs_regs.tgid_z_en != 0;
|
||||
@@ -745,7 +728,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
|
||||
void BuildStageStaticKey(const ShaderComputeInputInfo& info, std::vector<uint32_t>& key) {
|
||||
key.clear();
|
||||
key.push_back(info.workgroup_register);
|
||||
key.push_back(info.wave_size);
|
||||
key.push_back(info.wave_size | (static_cast<uint32_t>(info.float_mode) << 8u));
|
||||
key.push_back(info.host_subgroup_size);
|
||||
key.push_back(info.thread_ids_num);
|
||||
key.push_back(info.lds_size_dwords);
|
||||
|
||||
@@ -145,6 +145,7 @@ struct ShaderVertexInputInfo {
|
||||
};
|
||||
|
||||
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
|
||||
uint8_t float_mode = 0xc0;
|
||||
uint32_t dispatch_threads_num[3] = {0, 0, 0};
|
||||
bool group_id[3] = {false, false, false};
|
||||
bool dispatch_thread_dimensions = false;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
class QByteArray;
|
||||
class QWidget;
|
||||
@@ -29,6 +31,11 @@ private:
|
||||
QWidget* m_parent = nullptr;
|
||||
QNetworkAccessManager m_network;
|
||||
bool m_checking_updates = false;
|
||||
|
||||
// Answer from the primary update feed, kept while the GitHub fallback feed is
|
||||
// being consulted, so it can still be shown if the fallback request fails.
|
||||
QString m_primary_tag;
|
||||
QUrl m_primary_page_url;
|
||||
};
|
||||
|
||||
#endif // UPDATE_CHECKER_H
|
||||
|
||||
@@ -84,12 +84,30 @@ void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual)
|
||||
}
|
||||
reply->deleteLater();
|
||||
|
||||
if (!fallback &&
|
||||
(!info.error.isEmpty() || info.tag != QString::fromLatin1(KYTY_RELEASE_TAG))) {
|
||||
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
|
||||
return;
|
||||
if (!fallback) {
|
||||
const bool up_to_date =
|
||||
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
|
||||
if (!up_to_date) {
|
||||
if (info.error.isEmpty()) {
|
||||
// The primary feed is reachable but disagrees with the build tag
|
||||
// (e.g. a stale mirror); remember its answer in case the fallback
|
||||
// request also fails.
|
||||
m_primary_tag = info.tag;
|
||||
m_primary_page_url = info.page_url;
|
||||
}
|
||||
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
|
||||
// The fallback feed failed, but the primary feed already returned a
|
||||
// usable answer; prefer it over reporting a network error.
|
||||
info = {m_primary_tag, m_primary_page_url, {}};
|
||||
}
|
||||
m_primary_tag.clear();
|
||||
m_primary_page_url.clear();
|
||||
|
||||
m_checking_updates = false;
|
||||
emit CheckingChanged(false);
|
||||
ShowUpdateResult(info, manual);
|
||||
|
||||
@@ -950,9 +950,6 @@ public:
|
||||
if (deliver_seek_frame || sync_mode != 0) {
|
||||
return true;
|
||||
}
|
||||
if (audio_id) {
|
||||
return candidate.info.time_stamp <= last_audio_ts;
|
||||
}
|
||||
auto now = CurrentTimeNoLock();
|
||||
return now == 0 || candidate.info.time_stamp <= now;
|
||||
});
|
||||
@@ -999,7 +996,9 @@ public:
|
||||
out->details.audio.size = current_audio->info.details.audio.size;
|
||||
std::memcpy(out->details.audio.language_code,
|
||||
current_audio->info.details.audio.language_code, 4);
|
||||
last_audio_ts = out->time_stamp;
|
||||
start_time_ms = out->time_stamp;
|
||||
clock_start = std::chrono::steady_clock::now();
|
||||
paused_extra = {};
|
||||
RecordLoopBoundary(*current_audio);
|
||||
return true;
|
||||
}
|
||||
@@ -1064,7 +1063,6 @@ private:
|
||||
video_done = true;
|
||||
audio_done = true;
|
||||
seek_video_frame_pending = false;
|
||||
last_audio_ts = 0;
|
||||
last_output_loop_offset = 0;
|
||||
pending_loop_warnings = 0;
|
||||
}
|
||||
@@ -1541,11 +1539,14 @@ private:
|
||||
return false;
|
||||
}
|
||||
auto* dst = buffer->Get();
|
||||
std::memset(dst, 0, static_cast<size_t>(size));
|
||||
auto* c = dst + pitch * h;
|
||||
// PPSA02433 samples the padded columns; zero chroma would turn them green.
|
||||
std::memset(dst, s->codecpar->color_range == AVCOL_RANGE_JPEG ? 0 : 16,
|
||||
static_cast<size_t>(pitch) * h);
|
||||
std::memset(c, 128, static_cast<size_t>(pitch) * h / 2);
|
||||
for (int y = 0; y < src->height; y++) {
|
||||
std::memcpy(dst + y * pitch, nv12->data[0] + y * nv12->linesize[0], src->width);
|
||||
}
|
||||
auto* c = dst + pitch * h;
|
||||
for (int y = 0; y < src->height / 2; y++) {
|
||||
std::memcpy(c + y * pitch, nv12->data[1] + y * nv12->linesize[1], src->width);
|
||||
}
|
||||
@@ -1674,7 +1675,6 @@ private:
|
||||
int32_t trick_speed = AVPLAYER_TRICK_SPEED_NORMAL;
|
||||
uint32_t sync_mode = 0;
|
||||
uint64_t start_time_ms = 0;
|
||||
uint64_t last_audio_ts = 0;
|
||||
uint64_t last_output_loop_offset = 0;
|
||||
uint32_t pending_loop_warnings = 0;
|
||||
std::chrono::steady_clock::time_point clock_start {};
|
||||
|
||||
+22
-12
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+38
-6
@@ -133,6 +133,26 @@ static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseUint32(const std::string& value, uint32_t& out) {
|
||||
uint32_t number = 0;
|
||||
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
|
||||
if (error != std::errc {} || end != value.data() + value.size()) {
|
||||
return false;
|
||||
}
|
||||
out = number;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseInt32(const std::string& value, int32_t& out) {
|
||||
int32_t number = 0;
|
||||
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
|
||||
if (error != std::errc {} || end != value.data() + value.size()) {
|
||||
return false;
|
||||
}
|
||||
out = number;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseUserId(const std::string& value, int32_t& out) {
|
||||
int32_t user_id = 0;
|
||||
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id);
|
||||
@@ -246,9 +266,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
}
|
||||
options.game_patch = path;
|
||||
} else if (arg == "--screen-width") {
|
||||
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
|
||||
if (!ParseUint32(value, options.config.screen_width) ||
|
||||
options.config.screen_width == 0) {
|
||||
::printf("invalid screen width: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--screen-height") {
|
||||
options.config.screen_height = static_cast<uint32_t>(Common::ToInt32(value));
|
||||
if (!ParseUint32(value, options.config.screen_height) ||
|
||||
options.config.screen_height == 0) {
|
||||
::printf("invalid screen height: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--user-name") {
|
||||
if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) {
|
||||
::printf("invalid user name: must contain 1-%zu bytes\n",
|
||||
@@ -269,11 +297,15 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--gpu") {
|
||||
options.config.gpu_index = Common::ToInt32(value);
|
||||
if (!ParseInt32(value, options.config.gpu_index)) {
|
||||
::printf("invalid gpu index: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--vblank-frequency") {
|
||||
const int32_t vblank_frequency = Common::ToInt32(value);
|
||||
options.config.vblank_frequency =
|
||||
static_cast<uint32_t>(vblank_frequency < 0 ? 0 : vblank_frequency);
|
||||
if (!ParseUint32(value, options.config.vblank_frequency)) {
|
||||
::printf("invalid vblank frequency: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--console-language") {
|
||||
if (!ParseConsoleLanguage(value, options.config.console_language)) {
|
||||
::printf("invalid console language: %s\n", value.c_str());
|
||||
|
||||
@@ -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(¶m), &first) == OK,
|
||||
"first port create failed");
|
||||
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "common/lruCache.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool value, const char *message) {
|
||||
if (!value) {
|
||||
std::fprintf(stderr, "LruCacheTests: failed: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the objects reachable through ForEachItemBelow(), in visit order.
|
||||
std::vector<std::string> Collect(Common::LeastRecentlyUsedCache<std::string, uint64_t> &cache,
|
||||
uint64_t tick) {
|
||||
std::vector<std::string> visited;
|
||||
cache.ForEachItemBelow(tick, [&](const std::string &object) { visited.push_back(object); });
|
||||
return visited;
|
||||
}
|
||||
|
||||
void TestInsertAndVisitOrder() {
|
||||
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
|
||||
const auto a = cache.Insert("a", 10);
|
||||
const auto b = cache.Insert("b", 20);
|
||||
const auto c = cache.Insert("c", 30);
|
||||
|
||||
Check(a != b && b != c && a != c, "Insert returned duplicate ids");
|
||||
|
||||
const auto all = Collect(cache, UINT64_MAX);
|
||||
Check(all.size() == 3 && all[0] == "a" && all[1] == "b" && all[2] == "c",
|
||||
"inserted items are not visited in insertion order");
|
||||
|
||||
const auto below = Collect(cache, 20);
|
||||
Check(below.size() == 2 && below[0] == "a" && below[1] == "b",
|
||||
"ForEachItemBelow visited an item above the tick");
|
||||
|
||||
const auto none = Collect(cache, 5);
|
||||
Check(none.empty(), "ForEachItemBelow visited items below an empty tick");
|
||||
}
|
||||
|
||||
void TestEarlyExitCallback() {
|
||||
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
|
||||
(void)cache.Insert("a", 10);
|
||||
(void)cache.Insert("b", 20);
|
||||
(void)cache.Insert("c", 30);
|
||||
|
||||
std::vector<std::string> visited;
|
||||
cache.ForEachItemBelow(UINT64_MAX, [&](const std::string &object) {
|
||||
visited.push_back(object);
|
||||
return true; // stop after the first item
|
||||
});
|
||||
Check(visited.size() == 1 && visited[0] == "a", "boolean callback did not stop the walk");
|
||||
|
||||
std::vector<std::string> all_visited;
|
||||
cache.ForEachItemBelow(UINT64_MAX, [&](const std::string &object) {
|
||||
all_visited.push_back(object);
|
||||
return false;
|
||||
});
|
||||
Check(all_visited.size() == 3, "false-returning callback did not visit every item");
|
||||
}
|
||||
|
||||
void TestTouchReordersAndSkips() {
|
||||
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
|
||||
const auto a = cache.Insert("a", 10);
|
||||
const auto b = cache.Insert("b", 20);
|
||||
const auto c = cache.Insert("c", 30);
|
||||
|
||||
// Touching the last item must not change the order.
|
||||
cache.Touch(c, 35);
|
||||
auto order = Collect(cache, UINT64_MAX);
|
||||
Check(order.size() == 3 && order[0] == "a" && order[1] == "b" && order[2] == "c",
|
||||
"touching the newest item changed the order");
|
||||
|
||||
// An older or equal tick must be ignored.
|
||||
cache.Touch(a, 10);
|
||||
cache.Touch(a, 5);
|
||||
order = Collect(cache, UINT64_MAX);
|
||||
Check(order[0] == "a" && order[1] == "b" && order[2] == "c",
|
||||
"an ignored touch changed the order");
|
||||
|
||||
// Touching the oldest item moves it to the newest end.
|
||||
cache.Touch(a, 40);
|
||||
order = Collect(cache, UINT64_MAX);
|
||||
Check(order.size() == 3 && order[0] == "b" && order[1] == "c" && order[2] == "a",
|
||||
"touching an item did not move it to the newest end");
|
||||
|
||||
// The touched item now carries its new tick for tick filtering
|
||||
// (a=40, b=20, c=35 after the touches above). A cutoff of 30 detects
|
||||
// a stale c tick: c is visited only if Touch(c, 35) updated it.
|
||||
const auto below = Collect(cache, 30);
|
||||
Check(below.size() == 1 && below[0] == "b",
|
||||
"touched item did not adopt its new tick");
|
||||
|
||||
// Touch the middle item.
|
||||
cache.Touch(b, 45);
|
||||
order = Collect(cache, UINT64_MAX);
|
||||
Check(order.size() == 3 && order[0] == "c" && order[1] == "a" && order[2] == "b",
|
||||
"touching a middle item did not move it to the end");
|
||||
}
|
||||
|
||||
void TestFreeAndIdReuse() {
|
||||
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
|
||||
const auto a = cache.Insert("a", 10);
|
||||
const auto b = cache.Insert("b", 20);
|
||||
const auto c = cache.Insert("c", 30);
|
||||
|
||||
// Free the first and the last items.
|
||||
cache.Free(a);
|
||||
cache.Free(c);
|
||||
auto order = Collect(cache, UINT64_MAX);
|
||||
Check(order.size() == 1 && order[0] == "b", "Free left a freed item in the walk");
|
||||
|
||||
// Freed ids must be reused by later inserts.
|
||||
const auto d = cache.Insert("d", 40);
|
||||
Check(d == a || d == c, "Insert did not reuse a freed id");
|
||||
const auto e = cache.Insert("e", 50);
|
||||
Check(e != d && (e == a || e == c), "Insert reused the same freed id twice");
|
||||
|
||||
order = Collect(cache, UINT64_MAX);
|
||||
Check(order.size() == 3 && order[0] == "b" && order[1] == "d" && order[2] == "e",
|
||||
"reused ids are not visited in tick order");
|
||||
|
||||
// Touching a reused id must not disturb the other items.
|
||||
cache.Touch(d, 60);
|
||||
order = Collect(cache, UINT64_MAX);
|
||||
Check(order.size() == 3 && order[0] == "b" && order[1] == "e" && order[2] == "d",
|
||||
"touching a reused id changed the wrong items");
|
||||
|
||||
// Free everything, then rebuild.
|
||||
cache.Free(b);
|
||||
cache.Free(d);
|
||||
cache.Free(e);
|
||||
Check(Collect(cache, UINT64_MAX).empty(), "freeing every item left items in the walk");
|
||||
|
||||
const auto again = cache.Insert("again", 70);
|
||||
const auto order2 = Collect(cache, UINT64_MAX);
|
||||
Check(order2.size() == 1 && order2[0] == "again", "insert after a full free failed");
|
||||
cache.Touch(again, 70);
|
||||
Check(Collect(cache, 70).size() == 1, "touching a rebuilt item failed");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestInsertAndVisitOrder();
|
||||
TestEarlyExitCallback();
|
||||
TestTouchReordersAndSkips();
|
||||
TestFreeAndIdReuse();
|
||||
std::puts("LruCacheTests: all cases passed");
|
||||
return 0;
|
||||
}
|
||||
+263
-7
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15837,6 +15837,7 @@ private:
|
||||
device_features.shaderImageGatherExtended = true;
|
||||
device_features.sampleRateShading = true;
|
||||
device_features.shaderInt64 = true;
|
||||
device_features.shaderFloat64 = available_features.shaderFloat64;
|
||||
device_features.fillModeNonSolid = true;
|
||||
device_features.tessellationShader = true;
|
||||
device_info.pEnabledFeatures = &device_features;
|
||||
@@ -16400,6 +16401,11 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
|
||||
case Opcode::V_MAX3_F32:
|
||||
case Opcode::V_MED3_F32:
|
||||
case Opcode::V_DOT2C_F32_F16:
|
||||
case Opcode::V_CVT_F64_I32:
|
||||
case Opcode::V_CVT_F32_F64:
|
||||
case Opcode::V_RCP_F64:
|
||||
case Opcode::V_MUL_F64:
|
||||
case Opcode::V_FMA_F64:
|
||||
case Opcode::V_CVT_F32_I32:
|
||||
case Opcode::V_CVT_F32_U32:
|
||||
case Opcode::V_CVT_U32_F32:
|
||||
@@ -16634,6 +16640,8 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
|
||||
case Opcode::IMAGE_ATOMIC_AND:
|
||||
case Opcode::IMAGE_ATOMIC_OR:
|
||||
case Opcode::IMAGE_ATOMIC_XOR:
|
||||
case Opcode::IMAGE_ATOMIC_FMIN:
|
||||
case Opcode::IMAGE_ATOMIC_FMAX:
|
||||
case Opcode::IMAGE_SAMPLE:
|
||||
case Opcode::IMAGE_GATHER4_LZ:
|
||||
case Opcode::IMAGE_GATHER4_C:
|
||||
@@ -18937,6 +18945,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;
|
||||
|
||||
@@ -21655,6 +21695,157 @@ TestCase VectorSpecialF32FlushesDenormalInputs() {
|
||||
O::V_SQRT_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase VectorF64CapturedScreenSpaceShadows() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = "VectorF64CapturedScreenSpaceShadows";
|
||||
const auto store_pair = [&](u32 reg, double value) {
|
||||
const auto bits = std::bit_cast<uint64_t>(value);
|
||||
AppendStoreVgpr(&test.code, reg, static_cast<u32>(test.expected.size()));
|
||||
AppendStoreVgpr(&test.code, reg + 1,
|
||||
static_cast<u32>(test.expected.size() + 1));
|
||||
test.expected.insert(test.expected.end(), {static_cast<u32>(bits),
|
||||
static_cast<u32>(bits >> 32)});
|
||||
};
|
||||
for (const auto values :
|
||||
{std::array<int32_t, 3>{63, 127, 101},
|
||||
std::array<int32_t, 3>{16777217, -16777217, 123456789},
|
||||
std::array<int32_t, 3>{INT32_MIN, INT32_MAX, -31}}) {
|
||||
AppendVMovLiteral(&test.code, 7, static_cast<u32>(values[0]));
|
||||
AppendVMovLiteral(&test.code, 1, static_cast<u32>(values[1]));
|
||||
AppendVMovLiteral(&test.code, 9, static_cast<u32>(values[2]));
|
||||
// Exact FP64 instruction words from b90e2024732c6111, PCs 0xc8 through
|
||||
// 0x130.
|
||||
test.code.insert(test.code.end(), {0x7e060907u, 0x7e0a0901u, 0x7e020909u});
|
||||
store_pair(3, double(values[0]));
|
||||
store_pair(5, double(values[1]));
|
||||
store_pair(1, double(values[2]));
|
||||
test.code.push_back(0x7e065f03u);
|
||||
const double reciprocal = 1.0 / double(values[0]);
|
||||
store_pair(3, reciprocal);
|
||||
test.code.insert(test.code.end(), {0xd5650005u, 0x00020305u});
|
||||
const double product = double(values[1]) * double(values[2]);
|
||||
store_pair(5, product);
|
||||
test.code.insert(test.code.end(), {0xd54c0001u, 0x84060705u});
|
||||
const double result = std::fma(product, reciprocal, -double(values[2]));
|
||||
store_pair(1, result);
|
||||
test.code.push_back(0x7e041f01u);
|
||||
AppendStoreVgpr(&test.code, 2, static_cast<u32>(test.expected.size()));
|
||||
test.expected.push_back(std::bit_cast<u32>(static_cast<float>(result)));
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.opcodes = {O::V_MOV_B32, O::V_CVT_F64_I32, O::V_RCP_F64,
|
||||
O::V_MUL_F64, O::V_FMA_F64, O::V_CVT_F32_F64,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.required_spirv = {"OpCapability Float64",
|
||||
"OpTypeFloat 64",
|
||||
"OpFDiv",
|
||||
"Fma",
|
||||
"SignedZeroInfNanPreserve 64",
|
||||
"RoundingModeRTE 32"};
|
||||
test.ir_counts = {{"ConvertF64S32", 9},
|
||||
{"FPRecip64", 3},
|
||||
{"FPMul64", 3},
|
||||
{"FPFma64", 3},
|
||||
{"ConvertF32F64", 3}};
|
||||
|
||||
ShaderComputeInputInfo info{};
|
||||
std::vector<u32> normal_key, other_key;
|
||||
BuildStageStaticKey(info, normal_key);
|
||||
info.float_mode = 0xc4;
|
||||
BuildStageStaticKey(info, other_key);
|
||||
Require(test.name, "pipeline identity",
|
||||
normal_key != other_key && normal_key.size() == other_key.size(),
|
||||
"FP64 rounding modes must not share a compiled shader key");
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorF64ModesModifiersAndExec() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = "VectorF64ModesModifiersAndExec";
|
||||
const auto load_pair = [&](u32 reg, uint64_t bits) {
|
||||
AppendVMovLiteral(&test.code, reg, static_cast<u32>(bits));
|
||||
AppendVMovLiteral(&test.code, reg + 1, static_cast<u32>(bits >> 32));
|
||||
};
|
||||
const auto store_pair = [&](u32 reg, uint64_t bits) {
|
||||
AppendStoreVgpr(&test.code, reg, static_cast<u32>(test.expected.size()));
|
||||
AppendStoreVgpr(&test.code, reg + 1,
|
||||
static_cast<u32>(test.expected.size() + 1));
|
||||
test.expected.insert(test.expected.end(), {static_cast<u32>(bits),
|
||||
static_cast<u32>(bits >> 32)});
|
||||
};
|
||||
const auto store_word = [&](u32 reg, u32 bits) {
|
||||
AppendStoreVgpr(&test.code, reg, static_cast<u32>(test.expected.size()));
|
||||
test.expected.push_back(bits);
|
||||
};
|
||||
for (const auto [source, expected] :
|
||||
{std::pair<uint64_t, uint64_t>{0, 0x7ff0000000000000ull},
|
||||
{0x8000000000000000ull, 0xfff0000000000000ull},
|
||||
{0x7ff0000000000000ull, 0},
|
||||
{0xfff0000000000000ull, 0x8000000000000000ull},
|
||||
{0x0008000000000000ull, 0x7fe0000000000000ull},
|
||||
{0x7fe0000000000000ull, 0x0008000000000000ull}}) {
|
||||
load_pair(1, source);
|
||||
test.code.push_back(EncodeVop1(0x2f, 1, Vgpr(1)));
|
||||
store_pair(1, expected);
|
||||
}
|
||||
// A fused result retains the residual that separate multiply/add would round
|
||||
// to zero.
|
||||
load_pair(1, std::bit_cast<uint64_t>(1.0 + 0x1p-27));
|
||||
load_pair(3, std::bit_cast<uint64_t>(1.0 - 0x1p-27));
|
||||
AppendVop3(&test.code, 0x14c, 1, Vgpr(1), Vgpr(3), 243u);
|
||||
store_pair(1, std::bit_cast<uint64_t>(-0x1p-54));
|
||||
// Unaligned pairs, SGPR pairs, inline floats, literal highword expansion, ABS
|
||||
// then NEG.
|
||||
test.code.push_back(EncodeSop1(0x03, 8, 255));
|
||||
test.code.push_back(0u);
|
||||
test.code.push_back(EncodeSop1(0x03, 9, 255));
|
||||
test.code.push_back(0xbff00000u);
|
||||
AppendVop3(&test.code, 0x165, 1, 8u, 244u, 0, 1, 0, false, 0, 1);
|
||||
store_pair(1, std::bit_cast<uint64_t>(-2.0));
|
||||
AppendVop3(&test.code, 0x165, 1, 255u, 242u);
|
||||
test.code.push_back(0x40080000u);
|
||||
store_pair(1, std::bit_cast<uint64_t>(3.0));
|
||||
AppendVop3(&test.code, 0x165, 1, 248u, 242u);
|
||||
store_pair(1, 0x3fc45f306dc9c882ull);
|
||||
AppendVop3(&test.code, 0x184, 1, InlineU32(3), 0);
|
||||
store_pair(1, std::bit_cast<uint64_t>(3.0));
|
||||
AppendVop3(&test.code, 0x1af, 1, 244u, 0);
|
||||
store_pair(1, std::bit_cast<uint64_t>(0.5));
|
||||
// Preserve both destination words when EXEC is clear, restoring the original
|
||||
// live lanes.
|
||||
test.code.push_back(EncodeSop1(0x04, 12, 126));
|
||||
test.code.push_back(EncodeSop1(0x04, 126, InlineU32(0)));
|
||||
test.code.push_back(EncodeVop1(0x04, 1, InlineU32(9)));
|
||||
test.code.push_back(EncodeSop1(0x04, 126, 12));
|
||||
store_pair(1, std::bit_cast<uint64_t>(0.5));
|
||||
for (const auto [source, expected] :
|
||||
{std::pair<uint64_t, u32>{0x47f0000000000000ull, 0x7f7fffffu},
|
||||
{0xc7f0000000000000ull, 0xff7fffffu},
|
||||
{0x7ff0000000000000ull, 0x7f800000u},
|
||||
{0xfff0000000000000ull, 0xff800000u},
|
||||
{0x3800000000000000ull, 0},
|
||||
{0xb800000000000000ull, 0x80000000u},
|
||||
{0x3810000000000000ull, 0x00800000u},
|
||||
{0x3ff0000010000000ull, 0x3f800000u}}) {
|
||||
load_pair(1, source);
|
||||
AppendVop3(&test.code, 0x18f, 1, Vgpr(1), 0);
|
||||
store_word(1, expected);
|
||||
}
|
||||
load_pair(1, 0x7ff8123456789abcull);
|
||||
test.code.push_back(EncodeVop1(0x2f, 1, Vgpr(1)));
|
||||
AppendVMovLiteral(&test.code, 3, 0x7ff80000u);
|
||||
test.code.push_back(EncodeVop2(0x1b, 2, Vgpr(3), 2));
|
||||
store_word(2, 0x7ff80000u);
|
||||
AppendEnd(&test.code);
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::S_MOV_B64,
|
||||
O::V_CVT_F64_I32, O::V_RCP_F64, O::V_MUL_F64,
|
||||
O::V_FMA_F64, O::V_CVT_F32_F64, O::V_AND_B32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorRcpIflagF32IntegerReciprocal() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -28107,6 +28298,124 @@ TestCase ImageAtomicVariants() {
|
||||
return test;
|
||||
}
|
||||
|
||||
template <bool max_value> TestCase ImageAtomicFloatGlcAndExec() {
|
||||
using O = ShaderOpcode;
|
||||
std::vector<u32> code;
|
||||
constexpr u32 atomic_word = max_value ? 0xf07c0108u : 0xf0780108u;
|
||||
code.push_back(EncodeSop1(0x04, 12, 126)); // Preserve active workgroup lanes.
|
||||
AppendVMovU32(&code, 8, 2);
|
||||
AppendVMovU32(&code, 9, 1);
|
||||
AppendVMovLiteral(&code, 5, 0x40000000u); // 2.0
|
||||
// FMAX uses the captured GLC=0 instruction word.
|
||||
code.insert(code.end(), {atomic_word, 0x00010508u});
|
||||
AppendStoreVgpr(&code, 5, 0);
|
||||
AppendVMovLiteral(&code, 5, max_value ? 0x40800000u : 0x3f800000u);
|
||||
code.insert(code.end(), {atomic_word | 0x2000u, 0x00010508u}); // GLC=1.
|
||||
AppendStoreVgpr(&code, 5, 1);
|
||||
AppendVMovLiteral(&code, 5,
|
||||
max_value ? 0x41000000u : 0x3f000000u); // Inactive lane.
|
||||
code.push_back(EncodeSop1(0x04, 126, InlineU32(0)));
|
||||
code.insert(code.end(), {atomic_word | 0x2000u, 0x00010508u});
|
||||
code.push_back(EncodeSop1(0x04, 126, 12));
|
||||
AppendStoreVgpr(&code, 5, 2);
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test;
|
||||
test.name = max_value ? "ImageAtomicFMaxCapturedGlcAndExec"
|
||||
: "ImageAtomicFMinGlcAndExec";
|
||||
test.code = std::move(code);
|
||||
test.expected = {0x40000000u, 0x40000000u,
|
||||
max_value ? 0x41000000u : 0x3f000000u};
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B64,
|
||||
max_value ? O::IMAGE_ATOMIC_FMAX : O::IMAGE_ATOMIC_FMIN,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
const std::array<u32, 8> descriptor = {
|
||||
0x504a7c00u, 0xc1600000u, 0x0086c0efu, 0x91b80924u,
|
||||
0x00000000u, 0x00700080u, 0x00000000u, 0x00000000u};
|
||||
std::copy_n(descriptor.begin(), 8, test.user_data.begin() + 4);
|
||||
test.has_user_data = true;
|
||||
test.storage_image_r32ui = std::vector<u32>(16, 0);
|
||||
test.storage_image_r32ui[6] = max_value ? 0x3f800000u : 0x40800000u;
|
||||
test.expected_storage_image_r32ui = test.storage_image_r32ui;
|
||||
test.expected_storage_image_r32ui[6] = max_value ? 0x40800000u : 0x3f800000u;
|
||||
test.required_spirv = {"OpImageTexelPointer", "OpAtomicCompareExchange", "R32ui"};
|
||||
return test;
|
||||
}
|
||||
|
||||
template <bool max_value> TestCase ImageAtomicFloatSpecialValues() {
|
||||
using O = ShaderOpcode;
|
||||
// source, previous texel, expected minimum, expected maximum.
|
||||
const u32 cases[][4] = {
|
||||
{0x40000000u, 0x3f800000u, 0x3f800000u, 0x40000000u},
|
||||
{0xc0000000u, 0xbf800000u, 0xc0000000u, 0xbf800000u},
|
||||
{0x7f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u},
|
||||
{0xff800000u, 0x3f800000u, 0xff800000u, 0x3f800000u},
|
||||
{0x3f800000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u},
|
||||
{0x7fcabcdeu, 0x3f800000u, 0x3f800000u, 0x3f800000u},
|
||||
{0x3f800000u, 0x7fa54321u, 0x7fa54321u, 0x7fa54321u},
|
||||
{0x7faabcdeu, 0x3f800000u, 0x3f800000u, 0x3f800000u},
|
||||
{0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u},
|
||||
{0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u},
|
||||
{0x80000000u, 0x80000001u, 0x80000001u, 0x80000000u},
|
||||
{0x80000001u, 0x80000000u, 0x80000001u, 0x80000000u},
|
||||
{0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u},
|
||||
{0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u},
|
||||
};
|
||||
TestCase test;
|
||||
test.name = max_value ? "ImageAtomicFMaxSpecialValues"
|
||||
: "ImageAtomicFMinSpecialValues";
|
||||
test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt);
|
||||
test.has_user_data = true;
|
||||
test.storage_image_r32ui = std::vector<u32>(16, 0);
|
||||
test.expected_storage_image_r32ui = test.storage_image_r32ui;
|
||||
for (u32 i = 0; i < std::size(cases); i++) {
|
||||
AppendVMovU32(&test.code, 20, i & 3u);
|
||||
AppendVMovU32(&test.code, 21, i >> 2u);
|
||||
AppendVMovLiteral(&test.code, 0, cases[i][0]);
|
||||
test.code.push_back(EncodeMimg0(max_value ? 0x1f : 0x1e, 1, 0, true));
|
||||
test.code.push_back(EncodeMimg1(0, 20));
|
||||
AppendStoreVgpr(&test.code, 0, i);
|
||||
test.expected.push_back(cases[i][1]);
|
||||
test.storage_image_r32ui[i] = cases[i][1];
|
||||
test.expected_storage_image_r32ui[i] = cases[i][max_value ? 3 : 2];
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.opcodes = {O::V_MOV_B32,
|
||||
max_value ? O::IMAGE_ATOMIC_FMAX : O::IMAGE_ATOMIC_FMIN,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
template <bool max_value> TestCase ImageAtomicFloatContendedWorkgroup() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = max_value ? "ImageAtomicFMaxContendedWorkgroup"
|
||||
: "ImageAtomicFMinContendedWorkgroup";
|
||||
test.code.push_back(EncodeVop1(0x06, 1, Vgpr(0))); // Float thread_id.x.
|
||||
AppendVMovU32(&test.code, 20, 0);
|
||||
AppendVMovU32(&test.code, 21, 0);
|
||||
test.code.push_back(EncodeMimg0(max_value ? 0x1f : 0x1e, 1));
|
||||
test.code.push_back(EncodeMimg1(1, 20));
|
||||
AppendEnd(&test.code);
|
||||
test.opcodes = {O::V_CVT_F32_U32, O::V_MOV_B32,
|
||||
max_value ? O::IMAGE_ATOMIC_FMAX : O::IMAGE_ATOMIC_FMIN,
|
||||
O::S_ENDPGM};
|
||||
test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k32UInt);
|
||||
test.has_user_data = true;
|
||||
test.storage_image_r32ui = std::vector<u32>(16, 0);
|
||||
test.storage_image_r32ui[0] =
|
||||
max_value ? 0xc2c80000u : 0x42c80000u; // -/+100.0
|
||||
test.expected_storage_image_r32ui = test.storage_image_r32ui;
|
||||
test.expected_storage_image_r32ui[0] =
|
||||
max_value ? 0x427c0000u : 0u; // 63.0 / 0.0
|
||||
test.compute_info.threads_num[0] = 64;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ImageAtomicGlc0DoesNotReturnOldValue() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -28641,6 +28950,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(Vop1SdwaFfblCapturedHighWordSource);
|
||||
AddCase(Vop1SdwaNotCapturedByte0Source);
|
||||
AddCase(Vop1SdwaNotPreservesHighWordDestination);
|
||||
AddCase(Vop1SdwaNotPartialSourcesAndDestinations);
|
||||
AddCase(Vop1SdwaMovByteDestinations);
|
||||
AddCase(Vop2SdwaSubNcExactByte2Destination);
|
||||
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
|
||||
@@ -28721,6 +29031,8 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(CvtF32ToIntSaturatesNaNAndOutOfRange);
|
||||
AddCase(VectorSpecialF32FlushesDenormalInputs);
|
||||
AddCase(VectorRcpIflagF32IntegerReciprocal);
|
||||
AddCase(VectorF64CapturedScreenSpaceShadows);
|
||||
AddCase(VectorF64ModesModifiersAndExec);
|
||||
AddCase(VectorSinCosMaxFiniteSpecialCases);
|
||||
AddCase(VectorCompareOps);
|
||||
AddCase(VectorVop3CompareEqI64OnGpu);
|
||||
@@ -28918,6 +29230,12 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ImageAtomicSwapReturnsPreviousTexel);
|
||||
AddCase(ImageStoreAndAtomicUseSeparateBindings);
|
||||
AddCase(ImageAtomicVariants);
|
||||
AddCase(ImageAtomicFloatGlcAndExec<true>);
|
||||
AddCase(ImageAtomicFloatGlcAndExec<false>);
|
||||
AddCase(ImageAtomicFloatSpecialValues<true>);
|
||||
AddCase(ImageAtomicFloatSpecialValues<false>);
|
||||
AddCase(ImageAtomicFloatContendedWorkgroup<true>);
|
||||
AddCase(ImageAtomicFloatContendedWorkgroup<false>);
|
||||
AddCase(ImageAtomicGlc0DoesNotReturnOldValue);
|
||||
AddCase(MultipleWorkitemsGlobalId);
|
||||
AddCase(DispatcherIrreducibleControlFlow);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user