Compare commits

..

33 Commits

Author SHA1 Message Date
nmzik c86c8b4d93 audio: mix AudioOut2 objects and apply port gains
Co-authored-by: chenxiao07 <chenxiao07@gmail.com>
2026-10-08 01:47:34 +04:00
chenxiao07 e43aadf75f ampr: assign unique IDs to resolved file paths 2026-10-08 01:18:40 +04:00
shadowbeat070 9f25e3c23e shader: support partial SDWA multiply and byte-source moves 2026-10-08 01:10:49 +04:00
shadowbeat070 7881c69a54 shader: decode v_cmp_class_f16 2026-10-08 01:10:49 +04:00
shadowbeat070 9e666e4dd2 shader: support scalar byte sign extension 2026-10-08 01:10:49 +04:00
Nawid3333 c3a12397b0 tests: use function pointers in the concurrent backing read pairs (#1163)
TestConcurrentBackingReads (7e011fa5) builds its reader pairs with
std::pair {TryReadBacking, TryReadSparseBacking}. Class template argument
deduction then also considers pair's const T1& / const T2& constructors, which
deduce function types, and libstdc++ and libc++ reject a pair member of
function type ("data member instantiated with function type"), so
virtual_memory_allocation_tests no longer compiles on Linux and macOS. MSVC's
library accepts it, which is why Windows still builds.

Take the functions' addresses so the pairs hold function pointers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:56:44 +04:00
nmzik 45b6d2c597 memory: copy sparse backing in one traversal 2026-10-07 23:17:53 +04:00
nmzik 7e011fa565 memory: allow concurrent backing reads 2026-10-07 23:10:14 +04:00
nmzik 4d10ea17a0 graphics: streamline staging-buffer writes 2026-10-07 22:48:42 +04:00
nmzik 006875ed4f graphics: pause memory-tracking spin loops 2026-10-07 22:47:50 +04:00
nmzik a87889f032 graphics: process upload regions in one traversal 2026-10-07 22:47:50 +04:00
nmzik 9c79d9d298 memory: flatten direct and batch mapping paths 2026-10-07 22:47:50 +04:00
nmzik c30ac7c904 memory: avoid redundant Windows view protection 2026-10-07 22:47:50 +04:00
nmzik 038d2257de tests: enable image aliasing coverage on Linux 2026-10-07 21:35:28 +04:00
nmzik 190f838ea4 shader: preserve workgroup bounds for scalar image descriptor tables 2026-10-07 21:25:53 +04:00
nmzik 470669c677 graphics: make raw image synchronization an opt-in setting 2026-10-07 21:17:35 +04:00
Claxten dd754065d8 graphics: preserve full tiled array backing in image-to-buffer copies 2026-10-07 21:17:35 +04:00
Claxten 743fdc6990 graphics: synchronize raw storage reads with GPU image aliases 2026-10-07 21:17:35 +04:00
Claxten 9f83fd5f0c graphics: apply indirect draw offsets to guest user SGPRs 2026-10-07 21:17:35 +04:00
Claxten 5297abb7ac shader: run non-amplifying NGG programs in mesh workgroups 2026-10-07 21:17:35 +04:00
Claxten a8beac190f shader: evaluate scalar branch decisions across the complete guest wave 2026-10-07 21:17:35 +04:00
Claxten b96871e2bf shader: unpack compressed SNORM16 color exports as normalized signed values 2026-10-07 21:17:35 +04:00
Claxten 21a1346e27 graphics: route compact color exports through CB_SHADER_MASK 2026-10-07 21:17:35 +04:00
Claxten 3ba6ebae81 shader: initialize perspective sample barycentrics at SampleId 2026-10-07 20:44:46 +04:00
nmzik fbf6ae400c graphics: preserve depth array layout during storage reinterpretation 2026-10-07 20:44:46 +04:00
Pipe530 d2413fc2eb trophies: add viewer inspector and wrap toast titles 2026-10-07 17:22:44 +04:00
nmzik 96eb681b90 shader: add V_MIN3_U16 opcode 2026-10-07 16:22:30 +04:00
nmzik 26cce9909a shader: add BUFFER_LOAD_FORMAT_D16_X opcode 2026-10-07 16:22:30 +04:00
nmzik 5af370c257 shader: add BUFFER_STORE_FORMAT_D16_X opcode 2026-10-07 16:22:30 +04:00
nmzik 67d7db685a agc: new abi 2026-10-07 16:22:30 +04:00
nmzik bb98f26433 shader: add missing I64/U64 compare variants 2026-10-07 05:03:09 +04:00
dcave14 cc44a43ebc systemservice: add an option to report the notice screen as skipped (#1127)
* systemservice: add an option to report the notice screen as skipped

sceSystemServiceGetNoticeScreenSkipFlag always returned false. Return the
new skip_notice_screen option instead. Games that check this flag skip
their boot logo movies when it is set; Marvel's Wolverine, for example,
then does not queue movies/BootSequence/BootSequence.movie.

The option defaults to off, so behaviour is unchanged unless it is
enabled with --skip-notice-screen true or the launcher's "Skip boot
logos" checkbox. The launcher stores it with the other per-game settings,
so it is included in per-game config import and export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Shorten skip notice screen wording

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 04:21:27 +04:00
nmzik 2a18e66a4c gpu: validate depth copy state when drawing instead of decoding 2026-10-07 04:14:49 +04:00
64 changed files with 2886 additions and 638 deletions
+6 -9
View File
@@ -674,15 +674,12 @@ if(BUILD_TESTING)
add_test(NAME color_render_target_1d
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --rt1d-only)
if(WIN32)
# These tests still depend on the Windows multisample-depth path.
add_test(NAME texture_cache_image_overlap
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
add_test(NAME texture_cache_htile_clear
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
endif()
add_test(NAME texture_cache_image_overlap
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-overlap-only)
add_test(NAME texture_cache_htile_clear
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --htile-clear-only)
add_test(NAME buffer_cache_ranges
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --buffer-cache-range-only)
add_custom_target(kyty_tests DEPENDS
ime_dialog_tests
+8
View File
@@ -152,6 +152,10 @@ bool ReadbackLinearImagesEnabled() {
return g_config->readback_linear_images;
}
bool SyncRawImageBuffersEnabled() {
return g_config->sync_raw_image_buffers;
}
bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
@@ -164,6 +168,10 @@ bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
bool SkipNoticeScreen() {
return g_config->skip_notice_screen;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled() {
return g_config->red_zone_protection_enabled;
+4
View File
@@ -73,9 +73,11 @@ struct ConfigOptions {
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool playgo_hack_enabled = false;
bool skip_notice_screen = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
#endif
@@ -123,9 +125,11 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool SyncRawImageBuffersEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
bool PlayGoHackEnabled();
bool SkipNoticeScreen();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
#endif
@@ -23,6 +23,12 @@ enum class ContextStateOperation : uint32_t {
PushClear = 3,
};
struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
};
class Pm4Execution {
public:
[[nodiscard]] bool MadeProgress() const noexcept { return m_made_progress; }
@@ -79,10 +85,11 @@ public:
void DrawIndex(DrawIndexArgs args);
void DrawIndexOffset(uint32_t index_offset, uint32_t index_count);
void DrawIndexAuto(DrawAutoArgs args);
void DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed);
void DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed);
void DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr, uint32_t stride_in_bytes,
uint32_t draw_initiator, bool indexed);
IndirectDrawRegisters registers, uint32_t draw_initiator, bool indexed);
void WriteAtEndOfPipe32(uint32_t cache_policy, uint32_t event_write_dest,
uint32_t eop_event_type, uint32_t cache_action, uint32_t event_index,
uint32_t event_write_source, void* dst_gpu_addr, uint32_t value,
@@ -184,7 +184,6 @@ static HW::RenderControl DecodeRenderControl(uint32_t value) {
KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_STENCIL_TO_COLOR) != 0;
r.copy_centroid = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_CENTROID) != 0;
r.copy_sample = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_SAMPLE);
EXIT_NOT_IMPLEMENTED(r.copy_depth_to_color || r.copy_stencil_to_color);
return r;
}
@@ -1642,8 +1641,12 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
const auto data_offset = buffer[0];
const auto draw_initiator = buffer[3];
const bool indexed = (cmd_id == 0xc0032500);
// The native indexed packet enables the high-half first-index destination in word 3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
cp.DrawIndirect(data_offset, draw_initiator, indexed);
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
return 4;
}
@@ -1661,13 +1664,17 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
const auto stride_in_bytes = buffer[7];
const auto draw_initiator = buffer[8];
const bool indexed = (cmd_id == 0xc0083800);
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
if (count_indirect == 0) {
count_addr = nullptr;
}
cp.DrawIndirectMulti(data_offset, max_count_or_count, count_addr, stride_in_bytes,
draw_initiator, indexed);
registers, draw_initiator, indexed);
return 9;
}
+25 -4
View File
@@ -875,15 +875,32 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
DrawIndex({.index_count = index_count, .index_addr = index_addr});
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
uint32_t vertex_offset, uint32_t instance_offset,
uint32_t first_index) {
const auto write = [&](uint32_t location, uint32_t value) {
if (location == Pm4::SH_NOP) {
return;
}
EXIT_NOT_IMPLEMENTED(location >= Pm4::SH_NUM ||
g_hw_sh_indirect_func[location] == nullptr);
g_hw_sh_indirect_func[location](cp, location, value);
};
write(registers.vertex_offset, vertex_offset);
write(registers.instance_offset, instance_offset);
write(registers.index_offset, first_index);
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, draw_initiator, indexed);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, registers, draw_initiator, indexed);
}
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr,
uint32_t stride_in_bytes, uint32_t draw_initiator,
bool indexed) {
uint32_t stride_in_bytes, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
@@ -918,6 +935,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
if (!indexed) {
auto* args = reinterpret_cast<const DrawIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->start_vertex_location,
args->start_instance_location, 0);
m_num_instances = args->instance_count;
DrawIndexAuto({.vertex_count = args->vertex_count_per_instance,
.instance_count = args->instance_count,
@@ -928,6 +947,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
}
auto* args = reinterpret_cast<const DrawIndexedIndirectArgs*>(args_addr);
PatchIndirectDrawOffsets(*this, registers, args->base_vertex_location,
args->start_instance_location, args->start_index_location);
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args->start_index_location) * index_size);
+1 -2
View File
@@ -82,9 +82,8 @@ public:
static_assert(std::is_nothrow_invocable_v<RangeFunc&, uint64_t, uint64_t>);
static_assert(std::is_nothrow_invocable_v<UploadFunc&>);
CheckNotInUploadCallback();
Iterate<true>(vaddr, size, [](RegionManager*, uint64_t, uint64_t) {});
const auto* previous_upload_owner = std::exchange(s_upload_owner, this);
Iterate<false>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
Iterate<true>(vaddr, size, [&](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->lock.lock();
manager->ForEachModifiedRange<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset,
bytes, range_func);
+13 -1
View File
@@ -15,6 +15,12 @@
#include <mutex>
#include <vector>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
@@ -62,7 +68,13 @@ class SpinGuard final {
public:
explicit SpinGuard(std::atomic_flag& lock): m_lock(lock) {
while (m_lock.test_and_set(std::memory_order_acquire)) {
std::atomic_signal_fence(std::memory_order_seq_cst);
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#elif defined(_M_ARM64)
__yield();
#elif defined(__aarch64__)
asm volatile("yield");
#endif
}
}
~SpinGuard() { m_lock.clear(std::memory_order_release); }
+13 -1
View File
@@ -9,6 +9,12 @@
#include <mutex>
#include <utility>
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#ifndef NOMINMAX
#define NOMINMAX
@@ -32,7 +38,13 @@ public:
const auto thread = CurrentThread();
while (m_lock.test_and_set(std::memory_order_acquire)) {
EXIT_NOT_IMPLEMENTED(m_owner.load(std::memory_order_relaxed) == thread);
std::atomic_signal_fence(std::memory_order_seq_cst);
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#elif defined(_M_ARM64)
__yield();
#elif defined(__aarch64__)
asm volatile("yield");
#endif
}
m_owner.store(thread, std::memory_order_relaxed);
}
+29 -2
View File
@@ -34,9 +34,36 @@ void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void*
auto* bytes = static_cast<const uint8_t*>(source);
while (size != 0) {
const auto chunk = std::min(size, m_staging_buffer.Size());
// Copy waits for ring reuse and flushes the fresh host data before submission.
const auto offset = m_staging_buffer.Copy(bytes, chunk, 4);
buffer.CopyFrom(m_scheduler.Current(), m_staging_buffer, offset, buffer.Offset(address),
chunk, vk::AccessFlagBits::eHostWrite);
const auto destination_offset = buffer.Offset(address);
EXIT_IF(destination_offset > buffer.Size() || chunk > buffer.Size() - destination_offset);
m_scheduler.EndRendering();
const auto command = m_scheduler.Current().Handle();
vk::BufferMemoryBarrier2 before {};
before.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
before.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
before.dstStageMask = vk::PipelineStageFlagBits2::eTransfer;
before.dstAccessMask = vk::AccessFlagBits2::eTransferWrite;
before.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
before.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
before.buffer = buffer.Handle();
before.offset = destination_offset;
before.size = chunk;
vk::DependencyInfo dependency {};
dependency.dependencyFlags = vk::DependencyFlagBits::eByRegion;
dependency.bufferMemoryBarrierCount = 1;
dependency.pBufferMemoryBarriers = &before;
command.pipelineBarrier2(dependency);
const vk::BufferCopy copy {offset, destination_offset, chunk};
command.copyBuffer(m_staging_buffer.Handle(), buffer.Handle(), 1, &copy);
auto after = before;
after.srcStageMask = vk::PipelineStageFlagBits2::eTransfer;
after.srcAccessMask = vk::AccessFlagBits2::eTransferWrite;
after.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
after.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
dependency.pBufferMemoryBarriers = &after;
command.pipelineBarrier2(dependency);
bytes += chunk;
address += chunk;
size -= chunk;
+9 -10
View File
@@ -694,7 +694,9 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
!cached.info.HasMetadata();
// PPSA04264
// PPSA04264, PPSA04288
// A partial view retains the entire matching array layout. HTile belongs to
// the depth source, independently of the data slices copied into color storage.
const bool retain_cached_layout =
requested.samples == 1 && cached.info.samples == 1 && cached.backing.samples == 1 &&
requested.bytes_per_block == cached.info.bytes_per_block &&
@@ -713,7 +715,8 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.data.size / requested.resources.layers ==
cached.info.data.size / cached.info.resources.layers &&
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
!cached.info.HasMetadata();
(cached.info.metadata.kind == ImageMetadataKind::None ||
cached.info.metadata.kind == ImageMetadataKind::Htile);
bool recreate = cached.info.resources < requested.resources ||
requested.IsVolume() != cached.info.IsVolume();
switch (binding) {
@@ -738,8 +741,6 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
info.data = cached.info.data;
info.resources = cached.info.resources;
info.mip_layout = cached.info.mip_layout;
} else {
info.resources = std::max(requested.resources, cached.info.resources);
}
info.htile_clear_mask = 0;
const auto replacement_id = InsertImage(info);
@@ -1791,14 +1792,12 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uin
const auto available = buffer.Size() - buf_offset;
uint32_t levels = 0;
uint64_t copy_size = 0;
if (image.info.IsVolume()) {
// Volume mips contain strided block slices, so a mip's linear span cannot prove that
// every retained slice fits. Keep volume synchronization whole-image only.
if (!buffer.IsInBounds(image.info.data.address, image.info.data.size)) {
return false;
}
if (buffer.IsInBounds(image.info.data.address, image.info.data.size)) {
levels = image.info.resources.levels;
copy_size = image.info.data.size;
} else if (image.info.IsVolume() || image.info.resources.layers > 1) {
// Array and volume slices are strided across the full mip chain.
return false;
} else {
for (; levels < image.info.resources.levels; ++levels) {
const auto& mip = image.info.mip_layout[levels];
@@ -3,6 +3,7 @@
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
@@ -126,8 +127,9 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
if (size > graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange) {
EXIT("storage buffer range is unsupported\n");
}
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(address, size, resource.written,
resource.formatted, id);
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
address, size, resource.written,
resource.formatted || (resource.read && Config::SyncRawImageBuffersEnabled()), id);
const auto aligned_offset = Common::AlignDown(offset, alignment);
const auto adjustment = offset - aligned_offset;
const auto max_range = graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange;
+15 -7
View File
@@ -589,8 +589,12 @@ static uint32_t DrawColorOutputMask(const HW::Context& ctx) {
const auto& sh_regs = ctx.GetShaderRegisters();
const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask;
uint32_t output_mask = 0;
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
if (sh_regs.target_output_mode[slot] != 0 &&
for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) {
const auto slot = ShaderPixelExportTarget(sh_regs.m_cbShaderMask, index);
if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) {
break;
}
if (sh_regs.target_output_mode[index] != 0 &&
render_target_mask_slot(write_mask, slot) != 0) {
output_mask |= 1u << slot;
}
@@ -902,7 +906,7 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
if (state.ps_active) {
for (const auto& output: state.ps_input_info.stage.program->info.outputs) {
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt) {
mrt_mask |= 1u << output.index;
mrt_mask |= 1u << output.location;
}
}
}
@@ -1280,8 +1284,10 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
emit.vertex_offset = vertex_offset + args.base_vertex;
emit.first_instance = instance_offset;
// Native mesh prologs consume the offsets written to the packet's user SGPRs.
const bool mesh_indirect = indirect && state.vertex_info[0].logical_stage == ShaderType::Mesh;
emit.vertex_offset = mesh_indirect ? 0 : vertex_offset + args.base_vertex;
emit.first_instance = mesh_indirect ? 0 : instance_offset;
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source,
primitive_restart);
@@ -1369,8 +1375,10 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
indirect ? std::pair<int32_t, uint32_t> {0, args.first_instance}
: ResolveDrawOffsets(ucfg.GetIndexOffset(), state.vertex_info[0]);
DrawEmitInfo emit {};
emit.first_vertex = static_cast<uint32_t>(vertex_offset + static_cast<int32_t>(args.first_vertex));
emit.first_instance = instance_offset;
const bool mesh_indirect = indirect && state.vertex_info[0].logical_stage == ShaderType::Mesh;
emit.first_vertex = mesh_indirect ? 0 :
static_cast<uint32_t>(vertex_offset + static_cast<int32_t>(args.first_vertex));
emit.first_instance = mesh_indirect ? 0 : instance_offset;
DrawIndexBufferSource index_source {};
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false);
+64 -18
View File
@@ -195,6 +195,37 @@ std::mutex g_trophy_notification_mutex;
std::deque<TrophyNotification> g_trophy_notifications;
std::atomic<bool> g_trophy_notification_active {false};
std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_size, float width) {
std::vector<std::string_view> lines;
auto* const font = ImGui::GetFont();
for (;;) {
const auto newline = text.find('\n');
const auto paragraph = text.substr(0, newline);
auto line = paragraph.substr(0, 0);
for (auto start = paragraph.find_first_not_of(" \t\r"); start != std::string_view::npos;) {
const auto end = std::min(paragraph.find_first_of(" \t\r", start), paragraph.size());
const auto word = paragraph.substr(start, end - start);
const std::string_view candidate(line.empty() ? word.begin() : line.begin(),
word.end());
// Keep an oversized word intact; the toast's clip rectangle handles overflow.
if (!line.empty() && font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(),
0.0f, candidate.data(),
candidate.data() + candidate.size()).x > width) {
lines.push_back(line);
line = word;
} else {
line = candidate;
}
start = paragraph.find_first_not_of(" \t\r", end);
}
lines.push_back(line);
if (newline == std::string_view::npos) {
return lines;
}
text.remove_prefix(newline + 1);
}
}
void ClearInputEvents() {
std::scoped_lock lock(g_input_mutex);
g_input_events.clear();
@@ -1101,7 +1132,26 @@ struct SystemOverlay::Impl {
0.65f, 1.25f);
const float width = std::max(
0.0f, std::min(520.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
const float height = 104.0f * screen_scale;
constexpr std::array GRADE_COLORS {
IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255), IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
const bool has_grade = grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size());
const float pad = 10.0f * screen_scale;
const float base_height = 104.0f * screen_scale;
const float icon_size = base_height - 2.0f * pad;
const float text_offset = trophy_notification_image != nullptr
? pad + icon_size + 18.0f * screen_scale
: 20.0f * screen_scale;
const float title_offset = text_offset + (has_grade ? 30.0f * screen_scale : 0.0f);
const float title_size = 26.0f * screen_scale;
const float line_height = 30.0f * screen_scale;
const float text_padding = 16.0f * screen_scale;
const auto title_lines = WrapTrophyTitle(trophy_notification->name, title_size,
width - title_offset - text_padding);
const float block_height =
static_cast<float>(title_lines.size()) * line_height + 28.0f * screen_scale;
const float height = std::max(base_height, block_height + 2.0f * text_padding);
const float rounding = 14.0f * screen_scale;
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
@@ -1121,26 +1171,19 @@ struct SystemOverlay::Impl {
draw->AddRect(top_left, bottom_right, fade(255, 255, 255, 28), rounding,
ImDrawFlags_RoundCornersAll, 1.0f);
const float pad = 10.0f * screen_scale;
const float icon_size = height - 2.0f * pad;
float text_left = top_left.x + 20.0f * screen_scale;
const float text_left = top_left.x + text_offset;
if (trophy_notification_image != nullptr) {
const ImVec2 icon_min {top_left.x + pad, top_left.y + pad};
const ImVec2 icon_min {top_left.x + pad, top_left.y + (height - icon_size) * 0.5f};
const ImVec2 icon_max {icon_min.x + icon_size, icon_min.y + icon_size};
draw->AddImageRounded(trophy_notification_image->GetTexRef(), icon_min, icon_max,
{0, 0}, {1, 1}, fade(255, 255, 255, 255), 8.0f * screen_scale);
draw->AddRect(icon_min, icon_max, fade(255, 255, 255, 40), 8.0f * screen_scale,
ImDrawFlags_RoundCornersAll, 1.0f);
text_left = icon_max.x + 18.0f * screen_scale;
}
constexpr std::array GRADE_COLORS {IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255),
IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
const float title_y = top_left.y + height * 0.5f - 25.0f * screen_scale;
float title_x = text_left;
if (grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size())) {
const float title_y = top_left.y + (height - block_height) * 0.5f;
const float title_x = top_left.x + title_offset;
if (has_grade) {
// Small cup glyph tinted with the trophy grade.
const ImU32 cup = ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha);
const float u = screen_scale;
@@ -1155,12 +1198,15 @@ struct SystemOverlay::Impl {
draw->AddRectFilled({cx - 1.5f * u, cy + 2.0f * u}, {cx + 1.5f * u, cy + 7.0f * u}, cup);
draw->AddRectFilled({cx - 5.0f * u, cy + 7.0f * u}, {cx + 5.0f * u, cy + 10.0f * u}, cup,
1.5f * u);
title_x += 30.0f * u;
}
draw->PushClipRect(top_left, {bottom_right.x - 16.0f * screen_scale, bottom_right.y}, true);
draw->AddText(nullptr, 26.0f * screen_scale, {title_x, title_y},
fade(255, 255, 255, 255), trophy_notification->name.c_str());
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, title_y + 38.0f * screen_scale},
draw->PushClipRect(top_left, {bottom_right.x - text_padding, bottom_right.y}, true);
float line_y = title_y;
for (const auto line : title_lines) {
draw->AddText(nullptr, title_size, {title_x, line_y}, fade(255, 255, 255, 255),
line.data(), line.data() + line.size());
line_y += line_height;
}
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, line_y + 8.0f * screen_scale},
fade(176, 180, 192, 255), "Trophy earned!");
draw->PopClipRect();
}
@@ -280,6 +280,10 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
switch (inst.GetOpcode()) {
case IR::ValueOpcode::StoreCompletion: requirements.subgroup_barrier = true; break;
case IR::ValueOpcode::BvhIntersect: requirements.bvh = true; break;
case IR::ValueOpcode::ConditionRef:
requirements.subgroup_ballot |=
inst.Flags<CFG::BranchCondition>() != CFG::BranchCondition::ScalarInstruction;
break;
case IR::ValueOpcode::Ballot: requirements.subgroup_ballot = true; break;
case IR::ValueOpcode::DppMoveU32:
case IR::ValueOpcode::ReadFirstLane:
@@ -42,8 +42,9 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
EmitBinaryU32(state, spv::OpIMul, group, ConstantU32(state, size)));
}
const bool centroid = kind == IR::StageInputKind::BaryCoordSmoothCentroid;
const bool sample = kind == IR::StageInputKind::BaryCoordSmoothSample;
const auto variable = InputVariableForKind(
state, centroid ? IR::StageInputKind::BaryCoordSmooth : kind);
state, centroid || sample ? IR::StageInputKind::BaryCoordSmooth : kind);
if (variable == 0) {
return ConstantU32(state, 0);
}
@@ -76,15 +77,24 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
return bits;
}
if (centroid || kind == IR::StageInputKind::BaryCoordSmooth ||
if (centroid || sample || kind == IR::StageInputKind::BaryCoordSmooth ||
kind == IR::StageInputKind::BaryCoordNoPerspective) {
const auto value = state.builder.AllocateId();
const auto bits = state.builder.AllocateId();
if (centroid) {
if (centroid || sample) {
const auto coordinates = state.builder.AllocateId();
state.builder.RequireCapability(spv::CapabilityInterpolationFunction);
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
GlslStd450(state), GLSLstd450InterpolateAtCentroid, variable);
if (sample) {
const auto sample_id = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeI32(state), sample_id,
InputVariableForKind(state, IR::StageInputKind::SampleId));
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
GlslStd450(state), GLSLstd450InterpolateAtSample,
variable, sample_id);
} else {
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
GlslStd450(state), GLSLstd450InterpolateAtCentroid, variable);
}
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), value,
coordinates, component + 1u);
} else {
@@ -252,8 +262,12 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
bool uint_output) {
auto& state = ctx.state;
if (exp.compr && !uint_output) {
const auto unpack =
MrtOutputMode(state, exp) == 5u ? GLSLstd450UnpackUnorm2x16 : GLSLstd450UnpackHalf2x16;
auto unpack = GLSLstd450UnpackHalf2x16;
switch (MrtOutputMode(state, exp)) {
case 5: unpack = GLSLstd450UnpackUnorm2x16; break;
case 6: unpack = GLSLstd450UnpackSnorm2x16; break;
default: break;
}
uint32_t f32[4] = {ConstantF32(state, 0), ConstantF32(state, 0), ConstantF32(state, 0),
ConstantF32(state, 0x3f800000u)};
for (uint32_t pair = 0; pair < 2u; pair++) {
@@ -699,26 +713,18 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& inst) {
}
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.other_half == nullptr) return ctx.Arg(inst, 0);
// A native scalar branch makes one decision for both emulated wave halves.
if (ctx.half != 0) return ctx.other_half->Def(IR::Value(&inst));
// Scalar branches include lanes disabled by EXEC, which the branch body may restore.
if (ctx.other_half != nullptr && ctx.half != 0) return ctx.other_half->Def(IR::Value(&inst));
const auto kind = inst.Flags<CFG::BranchCondition>();
if (kind == CFG::BranchCondition::ScalarInstruction) return ctx.Arg(inst, 0);
const auto ballot = ctx.Ballot(inst.Arg(0));
const auto low = ctx.state.builder.AllocateId();
const auto high = ctx.state.builder.AllocateId();
const auto combined = ctx.state.builder.AllocateId();
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), low, ballot, 0);
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), high, ballot, 1);
const bool zero = kind == CFG::BranchCondition::ExecZero ||
kind == CFG::BranchCondition::VccZero || kind == CFG::BranchCondition::SccZero;
ctx.state.builder.AddFunction(zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr,
TypeU32(ctx.state), combined, low, high);
ctx.state.builder.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual,
TypeBool(ctx.state), result, combined,
ConstantU32(ctx.state, zero ? ~0u : 0u));
return result;
const auto ballot = ctx.Ballot(inst.Arg(0));
const auto reference = zero ? ctx.Ballot(IR::Value(true))
: ConstantU32CompositeZero(ctx.state, 4);
const auto compare = Binary(ctx.state, zero ? spv::OpIEqual : spv::OpINotEqual,
TypeBoolVector(ctx.state, 4), ballot, reference);
return Unary(ctx.state, zero ? spv::OpAll : spv::OpAny, TypeBool(ctx.state), compare);
}
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
@@ -716,18 +716,38 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
return LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, 0u), resource);
}
const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info);
return EmitValueOrDefaultIfCondition(
const auto value = EmitValueOrDefaultIfCondition(
ctx.state, plan.in_bounds, TypeU32(ctx.state),
FormattedOutOfBoundsValue(ctx, mem, plan, 1u),
[&]() { return LoadFormattedInBounds(ctx, mem, plan, 0u); });
if (mem.data_bits != 16u) {
return value;
}
if (info.type == Format::ComponentType::Uint) {
return EmitUMin32(ctx.state, value, ConstantU32(ctx.state, 0xffffu));
}
if (info.type == Format::ComponentType::Sint) {
const auto lower = EmitSMax32(ctx.state, value, ConstantU32(ctx.state, 0xffff8000u));
return EmitSMin32(ctx.state, lower, ConstantU32(ctx.state, 0x7fffu));
}
return EmitConvertF16F32(ctx.state, EmitBitCastF32U32(ctx.state, value));
});
}
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data) {
uint32_t component, uint32_t data, uint32_t data_bits) {
auto& state = ctx.state;
const auto bits = info.component_bits[component];
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else if (info.type == Format::ComponentType::Sint) {
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else {
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
}
}
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
@@ -779,7 +799,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
value, data, component);
}
}
value = EncodeFormattedStoreComponent(ctx, info, component, value);
value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits);
const auto bits = info.component_bits[component];
if (info.packed_bitfield) {
packed = EmitBitFieldInsert(
@@ -63,6 +63,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
};
constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
@@ -296,6 +298,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
}
DecodeVectorGpr(vdata, inst.dst);
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X) {
inst.dst.sdwa_sel = 4u;
}
DecodeVectorGpr(vaddr, inst.src0);
DecodeScalarSource(srsrc * 4u, pc, inst.src1);
DecodeScalarSource(soffset, pc, inst.src2);
@@ -53,6 +53,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
{0x16u, Opcode::S_FLBIT_I32_B64},
{0x19u, Opcode::S_SEXT_I32_I8},
{0x1au, Opcode::S_SEXT_I32_I16},
{0x1bu, Opcode::S_BITSET0_B32},
{0x1cu, Opcode::S_BITSET0_B64},
@@ -503,6 +503,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.dst).c_str(),
OperandToString(inst.src0).c_str()));
case Opcode::S_ABS_I32:
case Opcode::S_SEXT_I32_I8:
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
@@ -618,10 +619,12 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_LOAD_FORMAT_XY:
case Opcode::BUFFER_LOAD_FORMAT_XYZ:
case Opcode::BUFFER_LOAD_FORMAT_XYZW:
case Opcode::BUFFER_LOAD_FORMAT_D16_X:
case Opcode::BUFFER_STORE_FORMAT_X:
case Opcode::BUFFER_STORE_FORMAT_XY:
case Opcode::BUFFER_STORE_FORMAT_XYZ:
case Opcode::BUFFER_STORE_FORMAT_XYZW:
case Opcode::BUFFER_STORE_FORMAT_D16_X:
case Opcode::BUFFER_LOAD_UBYTE:
case Opcode::BUFFER_LOAD_USHORT:
case Opcode::BUFFER_LOAD_DWORD:
@@ -45,6 +45,7 @@ enum class Opcode {
S_CMOV_B64,
S_MOVK_I32,
S_ABS_I32,
S_SEXT_I32_I8,
S_SEXT_I32_I16,
S_ABSDIFF_I32,
S_BREV_B32,
@@ -269,6 +270,7 @@ enum class Opcode {
V_MIN3_I32,
V_MIN3_U32,
V_MIN3_F16,
V_MIN3_U16,
V_MAX3_F32,
V_MAX3_I32,
V_MAX3_U32,
@@ -399,6 +401,7 @@ enum class Opcode {
V_CMP_T_I32,
V_CMP_CLASS_F32,
V_CMPX_CLASS_F32,
V_CMP_CLASS_F16,
V_CMPX_CLASS_F16,
V_CMP_LT_I16,
V_CMP_EQ_I16,
@@ -455,19 +458,34 @@ enum class Opcode {
V_CMP_NE_U32,
V_CMP_GE_U32,
V_CMP_T_U32,
V_CMP_F_I64,
V_CMP_EQ_I64,
V_CMP_LT_I64,
V_CMP_LE_I64,
V_CMP_NE_I64,
V_CMP_T_I64,
V_CMP_F_U64,
V_CMP_LT_U64,
V_CMP_EQ_U64,
V_CMP_LE_U64,
V_CMP_GT_U64,
V_CMP_NE_U64,
V_CMP_GE_U64,
V_CMP_T_U64,
V_CMPX_F_I64,
V_CMPX_LT_I64,
V_CMPX_EQ_I64,
V_CMPX_LE_I64,
V_CMPX_NE_I64,
V_CMPX_T_I64,
V_CMPX_F_U64,
V_CMPX_LT_U64,
V_CMPX_EQ_U64,
V_CMPX_LE_U64,
V_CMPX_GT_U64,
V_CMPX_NE_U64,
V_CMPX_GE_U64,
V_CMPX_T_U64,
V_CMPX_LT_U32,
V_CMPX_EQ_U32,
V_CMPX_LE_U32,
@@ -490,10 +508,12 @@ enum class Opcode {
BUFFER_LOAD_FORMAT_XY,
BUFFER_LOAD_FORMAT_XYZ,
BUFFER_LOAD_FORMAT_XYZW,
BUFFER_LOAD_FORMAT_D16_X,
BUFFER_STORE_FORMAT_X,
BUFFER_STORE_FORMAT_XY,
BUFFER_STORE_FORMAT_XYZ,
BUFFER_STORE_FORMAT_XYZW,
BUFFER_STORE_FORMAT_D16_X,
BUFFER_LOAD_UBYTE,
BUFFER_LOAD_SBYTE,
BUFFER_LOAD_USHORT,
@@ -36,7 +36,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x05u, Opcode::V_SUBREV_F32},
{0x08u, Opcode::V_MUL_F32, Vop2SdwaProfile::Float32},
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerFullDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
{0x11u, Opcode::V_MIN_I32},
@@ -245,7 +245,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x88u, Opcode::V_CMP_CLASS_F32}, {0x89u, Opcode::V_CMP_LT_I16},
{0x8au, Opcode::V_CMP_EQ_I16}, {0x8bu, Opcode::V_CMP_LE_I16},
{0x8cu, Opcode::V_CMP_GT_I16}, {0x8du, Opcode::V_CMP_NE_I16},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x91u, Opcode::V_CMPX_LT_I32},
{0x8eu, Opcode::V_CMP_GE_I16}, {0x8fu, Opcode::V_CMP_CLASS_F16, false},
{0x91u, Opcode::V_CMPX_LT_I32},
{0x92u, Opcode::V_CMPX_EQ_I32}, {0x93u, Opcode::V_CMPX_LE_I32},
{0x94u, Opcode::V_CMPX_GT_I32}, {0x95u, Opcode::V_CMPX_NE_I32},
{0x96u, Opcode::V_CMPX_GE_I32}, {0x98u, Opcode::V_CMPX_CLASS_F32},
@@ -265,10 +266,18 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0xc7u, Opcode::V_CMP_T_U32}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa3u, Opcode::V_CMP_LE_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false},
{0xa0u, Opcode::V_CMP_F_I64, false}, {0xa7u, Opcode::V_CMP_T_I64, false},
{0xb0u, Opcode::V_CMPX_F_I64, false}, {0xb1u, Opcode::V_CMPX_LT_I64, false},
{0xb2u, Opcode::V_CMPX_EQ_I64, false}, {0xb3u, Opcode::V_CMPX_LE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xd2u, Opcode::V_CMPX_EQ_U32}, {0xd3u, Opcode::V_CMPX_LE_U32},
{0xd4u, Opcode::V_CMPX_GT_U32}, {0xd5u, Opcode::V_CMPX_NE_U32},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xe1u, Opcode::V_CMP_LT_U64, false},
{0xe0u, Opcode::V_CMP_F_U64, false}, {0xe7u, Opcode::V_CMP_T_U64, false},
{0xf0u, Opcode::V_CMPX_F_U64, false}, {0xf1u, Opcode::V_CMPX_LT_U64, false},
{0xf2u, Opcode::V_CMPX_EQ_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf6u, Opcode::V_CMPX_GE_U64, false}, {0xf7u, Opcode::V_CMPX_T_U64, false},
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe3u, Opcode::V_CMP_LE_U64, false},
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
{0xe6u, Opcode::V_CMP_GE_U64, false}, {0xf3u, Opcode::V_CMPX_LE_U64, false},
@@ -310,6 +319,7 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
{0x152u, Opcode::V_MIN3_I32},
{0x153u, Opcode::V_MIN3_U32},
{0x351u, Opcode::V_MIN3_F16},
{0x353u, Opcode::V_MIN3_U16},
{0x154u, Opcode::V_MAX3_F32},
{0x155u, Opcode::V_MAX3_I32},
{0x156u, Opcode::V_MAX3_U32},
@@ -460,8 +470,9 @@ bool IsNativeVop3F16TernaryOpcode(Opcode opcode) {
opcode == Opcode::V_MED3_F16 || opcode == Opcode::V_FMA_F16;
}
bool IsNativeVop3I16TernaryOpcode(Opcode opcode) {
return opcode == Opcode::V_MED3_I16 || opcode == Opcode::V_MAD_I16;
bool IsNativeVop3B16TernaryOpcode(Opcode opcode) {
return opcode == Opcode::V_MIN3_U16 || opcode == Opcode::V_MED3_I16 ||
opcode == Opcode::V_MAD_I16;
}
bool IsNativeVop3B16BinaryOpcode(Opcode opcode) {
@@ -568,7 +579,7 @@ struct Vop1SdwaRule {
constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_MOV_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{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},
@@ -919,6 +930,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_NEQ_F16:
case Opcode::V_CMPX_NLT_F16:
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMP_CLASS_F16:
case Opcode::V_CMPX_CLASS_F16:
case Opcode::V_CMPX_CLASS_F32: return true;
default: return false;
@@ -1367,7 +1379,7 @@ void ApplyNativeVop3TernaryModifiers(Instruction& inst, uint32_t op_sel, uint32_
inst.dst.sdwa_sel = ((op_sel & 0x8u) != 0) ? 5u : 4u;
}
void ApplyNativeVop3I16TernarySelectors(Instruction& inst, uint32_t op_sel) {
void ApplyNativeVop3B16TernarySelectors(Instruction& inst, uint32_t op_sel) {
Operand* sources[] = {&inst.src0, &inst.src1, &inst.src2};
for (uint32_t i = 0; i < 3u; i++) {
sources[i]->op_sel = ((op_sel >> i) & 1u) != 0;
@@ -1486,7 +1498,7 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi
// Clamp is applied by WriteF16 for every f16 ternary; omod only for V_FMA_F16.
return opcode != Opcode::V_FMA_F16 && omod != 0u;
}
if (IsNativeVop3I16TernaryOpcode(opcode)) {
if (IsNativeVop3B16TernaryOpcode(opcode)) {
return abs != 0u || (clamp != 0u && !clamp_modifier) || omod != 0u || neg != 0u;
}
if (IsNativeVop3B16BinaryOpcode(opcode)) {
@@ -1555,9 +1567,20 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_GT_U32:
case Opcode::V_CMPX_NE_U32:
case Opcode::V_CMPX_GE_U32:
case Opcode::V_CMPX_F_I64:
case Opcode::V_CMPX_LT_I64:
case Opcode::V_CMPX_EQ_I64:
case Opcode::V_CMPX_LE_I64:
case Opcode::V_CMPX_NE_I64:
case Opcode::V_CMPX_T_I64:
case Opcode::V_CMPX_F_U64:
case Opcode::V_CMPX_LT_U64:
case Opcode::V_CMPX_EQ_U64:
case Opcode::V_CMPX_LE_U64:
case Opcode::V_CMPX_GT_U64:
case Opcode::V_CMPX_NE_U64:
case Opcode::V_CMPX_GE_U64:
case Opcode::V_CMPX_T_U64:
case Opcode::V_CMPX_LT_I16:
case Opcode::V_CMPX_EQ_I16:
case Opcode::V_CMPX_LE_I16:
@@ -1712,7 +1735,7 @@ void DecodeVop3(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const bool vop3b_uses_sdst = carry_in_out || vop3b_carry_out || vop3b_mad_u64;
const bool mad_mix = false;
const bool f16_ternary = IsNativeVop3F16TernaryOpcode(inst.opcode);
const bool i16_ternary = IsNativeVop3I16TernaryOpcode(inst.opcode);
const bool b16_ternary = IsNativeVop3B16TernaryOpcode(inst.opcode);
const bool b16_binary = IsNativeVop3B16BinaryOpcode(inst.opcode);
const bool pack_b32_f16 = inst.opcode == Opcode::V_PACK_B32_F16;
const bool permlane = IsPermlaneOpcode(inst.opcode);
@@ -1823,8 +1846,8 @@ void DecodeVop3(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
ApplyNativeVop3TernaryModifiers(inst, op_sel, abs, neg);
} else if (f16_ternary) {
ApplyNativeVop3TernaryModifiers(inst, op_sel, abs, neg);
} else if (i16_ternary) {
ApplyNativeVop3I16TernarySelectors(inst, op_sel);
} else if (b16_ternary) {
ApplyNativeVop3B16TernarySelectors(inst, op_sel);
} else if (b16_binary) {
ApplyNativeVop3B16BinaryModifiers(inst, op_sel);
} else if (pack_b32_f16) {
@@ -31,6 +31,7 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
inst.opcode == Decoder::Opcode::V_CMP_EQ_I64 ||
inst.opcode == Decoder::Opcode::V_CMPX_EQ_I64 ||
inst.opcode == Decoder::Opcode::V_CMP_NE_I64 ||
inst.opcode == Decoder::Opcode::V_CMPX_NE_I64;
const auto read = [&](const Decoder::Operand& operand) {
@@ -31,11 +31,12 @@ void Translator::V_MAD_I16(const Decoder::Instruction& inst) {
Write16Bits(DestinationOperand(inst), ir.BitwiseAnd(result, IR::U32(IR::Value(0xffffu))));
}
void Translator::V_MED3_I16(const Decoder::Instruction& inst) {
void Translator::Integer16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool sign) {
const auto result = IR::U32(ir.Emit(
IR::ValueOpcode::SMedTri32, {ReadU16AsU32(inst.src0, true), ReadU16AsU32(inst.src1, true),
ReadU16AsU32(inst.src2, true)}));
Write16Bits(DestinationOperand(inst), ir.BitwiseAnd(result, IR::U32(IR::Value(0xffffu))));
opcode, {ReadU16AsU32(inst.src0, sign), ReadU16AsU32(inst.src1, sign),
ReadU16AsU32(inst.src2, sign)}));
Write16Bits(DestinationOperand(inst), result);
}
void Translator::PackedInteger16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
@@ -187,7 +187,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
if (decoded.family == Decoder::Family::MUBUF || decoded.family == Decoder::Family::MTBUF) {
const bool store_or_atomic =
(decoded.opcode >= Decoder::Opcode::BUFFER_STORE_FORMAT_X &&
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_FORMAT_XYZW) ||
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_FORMAT_D16_X) ||
(decoded.opcode >= Decoder::Opcode::BUFFER_STORE_BYTE &&
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_DWORDX4) ||
(decoded.opcode >= Decoder::Opcode::TBUFFER_STORE_FORMAT_X &&
@@ -427,7 +427,7 @@ void Translator::S_LOAD(const Decoder::Instruction& inst, bool raw) {
void Translator::BUFFER_LOAD(const Decoder::Instruction& inst) {
const auto memory = MemoryInfoFromDecoded(inst);
IR::ValueOpcode opcode;
const auto bits = memory.data_bits;
const auto bits = memory.formatted ? 32u : memory.data_bits;
const auto sign = memory.data_signed;
switch (bits) {
case 8u: opcode = IR::ValueOpcode::LoadBufferU8; break;
@@ -473,7 +473,8 @@ void Translator::BUFFER_STORE(const Decoder::Instruction& inst) {
const auto data = ReadU32(data_src);
IR::ValueOpcode opcode;
IR::Value value;
switch (memory.data_bits) {
// Formatted stores carry packed VGPR bits; the format determines their conversion.
switch (memory.formatted ? 32u : memory.data_bits) {
case 8u:
opcode = IR::ValueOpcode::StoreBufferU8;
value = NarrowSubdword(data, 8u);
@@ -980,6 +981,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XY:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XYZ:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XYZW:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_D16_X:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_X:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_XY:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_XYZ:
@@ -995,6 +997,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_STORE_FORMAT_XY:
case Decoder::Opcode::BUFFER_STORE_FORMAT_XYZ:
case Decoder::Opcode::BUFFER_STORE_FORMAT_XYZW:
case Decoder::Opcode::BUFFER_STORE_FORMAT_D16_X:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_X:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_XY:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_XYZ:
@@ -134,10 +134,14 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_ABS_I32:
return SimpleInteger(inst, IR::ValueOpcode::IAbs32, IR::Type::U32, false, false, true);
case O::S_SEXT_I32_I16:
case O::S_SEXT_I32_I8:
case O::S_SEXT_I32_I16: {
// Sign extension leaves SCC unchanged.
const auto bits = inst.opcode == O::S_SEXT_I32_I8 ? 8u : 16u;
WriteOperand(inst.dst, IR::U32(ir.Emit(IR::ValueOpcode::BitFieldSExtract,
{ReadU32(inst.src0), IR::Value(0u), IR::Value(16u)})));
{ReadU32(inst.src0), IR::Value(0u), IR::Value(bits)})));
return;
}
case O::S_MUL_I32:
case O::S_MULK_I32:
return SimpleInteger(inst, IR::ValueOpcode::IMul32, IR::Type::U32, false, false, false);
@@ -889,9 +889,16 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
case Decoder::Opcode::V_CMP_GT_U64:
case Decoder::Opcode::V_CMP_NE_U64:
case Decoder::Opcode::V_CMP_GE_U64:
case Decoder::Opcode::V_CMPX_LT_I64:
case Decoder::Opcode::V_CMPX_EQ_I64:
case Decoder::Opcode::V_CMPX_LE_I64:
case Decoder::Opcode::V_CMPX_NE_I64:
case Decoder::Opcode::V_CMPX_LT_U64:
case Decoder::Opcode::V_CMPX_EQ_U64:
case Decoder::Opcode::V_CMPX_LE_U64:
case Decoder::Opcode::V_CMPX_GT_U64:
case Decoder::Opcode::V_CMPX_NE_U64:
case Decoder::Opcode::V_CMPX_GE_U64:
include_vector(inst.src0, 2u);
include_vector(inst.src1, 2u);
break;
@@ -1130,7 +1137,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
} else if (options.stage == ShaderType::Mesh) {
const auto& mesh = options.input_info.vertex->mesh;
EXIT_NOT_IMPLEMENTED(mesh.primitives_per_group == 0u || mesh.vertices_per_group > 64u ||
EXIT_NOT_IMPLEMENTED(mesh.primitives_per_group == 0u || mesh.vertices_per_group > 0x1ffu ||
mesh.primitives_per_group > 0x1ffu ||
total_threads > 15u * options.wave_size);
const auto u32 = [](uint32_t value) { return IR::U32(IR::Value(value)); };
const auto draw = [&](uint32_t index) {
@@ -1166,6 +1174,10 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
const auto wave_info = entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(wave, u32(24)),
u32(((total_threads + options.wave_size - 1u) /
options.wave_size) << 28u));
entry_ir.SetScalarReg(
static_cast<IR::ScalarReg>(2),
entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(vertices, u32(12)),
entry_ir.ShiftLeftLogical(primitives, u32(22))));
entry_ir.SetScalarReg(
static_cast<IR::ScalarReg>(3),
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
@@ -1274,6 +1286,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
};
barycentric_pair(ps->ps_perspective_center_vgpr, IR::StageInputKind::BaryCoordSmooth);
barycentric_pair(ps->ps_perspective_sample_vgpr,
IR::StageInputKind::BaryCoordSmoothSample);
barycentric_pair(ps->ps_perspective_centroid_vgpr,
IR::StageInputKind::BaryCoordSmoothCentroid);
uint32_t reg = ps->ps_system_input_base;
@@ -166,8 +166,8 @@ private:
void FloatCube(const Decoder::Instruction& inst, uint32_t result_kind);
void Integer16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool arithmetic);
void Integer16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool sign);
void Integer16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool sign);
void V_MAD_I16(const Decoder::Instruction& inst);
void V_MED3_I16(const Decoder::Instruction& inst);
void PackedInteger16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool arithmetic);
void PackedInteger16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
@@ -96,16 +96,34 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
return;
case O::V_CMP_F_I64:
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_I64:
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_T_I64:
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_EQ_I64:
case O::V_CMPX_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_NE_I64:
case O::V_CMP_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
@@ -118,9 +136,15 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
@@ -130,6 +154,9 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
@@ -347,6 +374,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
@@ -395,7 +423,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_SUB_NC_I16: return Integer16Binary(inst, IR::ValueOpcode::ISub32, false);
case O::V_MUL_LO_U16: return Integer16Binary(inst, IR::ValueOpcode::IMul32, false);
case O::V_MAD_I16: return V_MAD_I16(inst);
case O::V_MED3_I16: return V_MED3_I16(inst);
case O::V_MED3_I16: return Integer16Ternary(inst, IR::ValueOpcode::SMedTri32, true);
case O::V_MIN3_U16: return Integer16Ternary(inst, IR::ValueOpcode::UMinTri32, false);
case O::V_MIN_I16: return Integer16Binary(inst, IR::ValueOpcode::SMin32, true);
case O::V_MAX_I16: return Integer16Binary(inst, IR::ValueOpcode::SMax32, true);
case O::V_MIN_U16: return Integer16Binary(inst, IR::ValueOpcode::UMin32, false);
@@ -196,6 +196,7 @@ enum class StageInputKind {
Layer,
SampleId,
BaryCoordSmooth,
BaryCoordSmoothSample,
BaryCoordSmoothCentroid,
BaryCoordNoPerspective,
WorkgroupId,
@@ -1731,7 +1731,7 @@ private:
indirect.table_immediate = table_immediate;
indirect.table_stride = table_stride;
indirect.workgroup_axis = WorkgroupAxis(key);
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
if (indirect.workgroup_axis != UINT32_MAX && !plan.table_indexed) {
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
// only the image handle is projected onto the bounded workgroup key.
plan.retain_reads = true;
@@ -143,6 +143,7 @@ void ValidateValueReferences(const Program& program, ShaderStageInputInfo input_
}
break;
case StageInputKind::BaryCoordSmooth:
case StageInputKind::BaryCoordSmoothSample:
case StageInputKind::BaryCoordSmoothCentroid:
case StageInputKind::BaryCoordNoPerspective:
if (component >= 2u) {
@@ -299,6 +300,9 @@ void CollectBuiltinInputs(const Program& program, ShaderInfo& info) {
break;
case StageInputKind::Layer: AddInput(info, kind, 0, 1, "gl_Layer"); break;
case StageInputKind::SampleId: AddInput(info, kind, 0, 1, "gl_SampleID"); break;
case StageInputKind::BaryCoordSmoothSample:
AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID");
[[fallthrough]];
case StageInputKind::BaryCoordSmooth:
case StageInputKind::BaryCoordSmoothCentroid:
AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR");
@@ -386,16 +390,23 @@ void CollectOutputs(const Program& program, ShaderStageInputInfo input_info, Sha
AddOutput(info, StageOutputKind::Parameter, export_info.index,
export_info.index, fmt::format("out_param_{}", export_info.index));
break;
case ExportTargetKind::Mrt:
case ExportTargetKind::Mrt: {
if (alpha_remap && export_info.index != 0) {
break;
}
AddOutput(info, StageOutputKind::Mrt, export_info.index, export_info.index,
const auto slot = input_info.pixel->dual_source_blending
? 0u : ShaderPixelExportTarget(input_info.pixel->target_shader_mask,
export_info.index);
if (slot >= 8) {
break;
}
AddOutput(info, StageOutputKind::Mrt, export_info.index, slot,
fmt::format("out_mrt_{}", export_info.index));
if (alpha_remap) {
AddOutput(info, StageOutputKind::Mrt, 1, 1, "out_mrt_1");
AddOutput(info, StageOutputKind::Mrt, 1, 0, "out_mrt_1");
}
break;
}
default: break;
}
}
+25 -10
View File
@@ -525,6 +525,9 @@ static void ShaderGetStaticInputInfoPS(
EXIT_NOT_IMPLEMENTED(ps_info.input_num > std::size(ps_info.interpolator_settings));
ps_info.ps_system_input_base = ShaderCalcPsSystemInputBase(sh);
const uint32_t active_inputs = sh.ps_input_ena & sh.ps_input_addr;
if ((active_inputs & 0x00000001u) != 0) {
ps_info.ps_perspective_sample_vgpr = 0;
}
if ((active_inputs & 0x00000002u) != 0) {
ps_info.ps_perspective_center_vgpr = (active_inputs & 0x00000001u) != 0 ? 2u : 0u;
}
@@ -558,10 +561,12 @@ static void ShaderGetStaticInputInfoPS(
ps_info.interpolator_settings[i] = sh.ps_interpolator_settings[i];
}
ps_info.target_shader_mask = sh.m_cbShaderMask;
for (int i = 0; i < 8; i++) {
const auto slot = ShaderPixelExportTarget(ps_info.target_shader_mask, i);
ps_info.target_output_mode[i] = sh.target_output_mode[i];
ps_info.target_export_mapping[i] = sh.target_output_mode[i] != 0
? target_export_mapping[i]
ps_info.target_export_mapping[i] = sh.target_output_mode[i] != 0 && slot < 8
? target_export_mapping[slot]
: Prospero::ColorComponentMapping {};
}
}
@@ -649,6 +654,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(info.ps_system_input_base);
key.push_back(info.custom_interpolation_mask);
key.push_back(info.ps_perspective_center_vgpr);
key.push_back(info.ps_perspective_sample_vgpr);
key.push_back(info.ps_perspective_centroid_vgpr);
key.push_back(static_cast<uint32_t>(info.ps_pos_x));
key.push_back(static_cast<uint32_t>(info.ps_pos_y));
@@ -664,6 +670,8 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.dual_source_blending));
key.push_back(static_cast<uint32_t>(info.alpha_blend_source));
key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode));
const auto mask = info.target_shader_mask;
key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u);
for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) {
uint32_t packed = 0;
for (uint32_t i = 0; i < 4u; i++) {
@@ -697,11 +705,14 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
const auto& sh = context.GetShaderRegisters();
const auto [data, hash] = ShaderGetMappedData(regs.es_regs.data_addr, "ShaderGetInputInfoVS():");
const bool merged = (context.GetShaderStages() & 0x20u) != 0;
// GS_EN controls amplification, not whether an NGG shader uses a workgroup.
const bool native_ngg = !merged && data.type == Prospero::ShaderBinaryType::kGs &&
regs.gs_regs.rsrc2.lds_size != 0;
auto params = GetShaderParams(
regs.es_regs.data_addr, hash,
std::span<const uint32_t>(regs.gs_user_sgpr.value, regs.gs_regs.rsrc2.user_sgpr), data,
merged ? 8u : 0u);
if (!merged) {
merged || native_ngg ? 8u : 0u);
if (!merged && !native_ngg) {
if (!ShaderGetStaticVertexInputInfo(regs.es_regs.data_addr, regs.gs_user_sgpr,
regs.gs_regs.rsrc2.user_sgpr, sh, data, info)) {
EXIT("failed to prepare vertex shader program\n");
@@ -746,7 +757,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
group.primitive_group_size != 1u || group.vertex_group_size != 1u ||
user_vgpr.vgpr1 || user_vgpr.vgpr2 || user_vgpr.vgpr3 ||
mesh.max_vertices != sh.m_vgtGsMaxVertOut);
} else {
} else if (merged) {
EXIT_NOT_IMPLEMENTED(regs.gs_regs.rsrc1.gs_vgpr_component_count != 3u ||
regs.gs_regs.rsrc2.es_vgpr_component_count != 3u);
}
@@ -755,7 +766,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
user_config.GetPrimType() != Prospero::PrimitiveType::kTriFan &&
user_config.GetPrimType() != Prospero::PrimitiveType::kTriStrip &&
user_config.GetPrimType() != Prospero::PrimitiveType::kTriList) ||
sh.m_vgtGsOutPrimType != 2u || sh.m_vgtGsMaxVertOut < 3u ||
sh.m_vgtGsOutPrimType != 2u || (!native_ngg && sh.m_vgtGsMaxVertOut < 3u) ||
group.vertex_group_size < mesh.InputPrimitiveSize() ||
mesh.max_vertices == 0u) {
EXIT("unsupported GS assembly: input=%u output=%u vertices=%u GE=%u/%u max_output=%u\n",
@@ -763,14 +774,16 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
group.primitive_group_size, group.vertex_group_size, mesh.max_vertices);
}
mesh.max_primitives = mesh.fast_launch ? mesh.max_vertices :
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
mesh.primitives_per_group = std::min({static_cast<uint32_t>(group.primitive_group_size),
mesh.InputPrimitiveCount(group.vertex_group_size),
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
native_ngg ? mesh.max_primitives :
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
EXIT_IF(mesh.primitives_per_group == 0u);
mesh.vertices_per_group = mesh.InputVertexCount(mesh.primitives_per_group);
mesh.threads_num[0] =
((mesh.max_vertices + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
const auto threads = native_ngg && !mesh.fast_launch ? mesh.vertices_per_group : mesh.max_vertices;
mesh.threads_num[0] = ((threads + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
mesh.threads_num[1] = mesh.threads_num[2] = 1u;
return params;
}
@@ -910,6 +923,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
"\t ps_system_input_base = %u\n"
"\t custom_interpolation_mask = 0x%08" PRIx32 "\n"
"\t ps_perspective_center_vgpr = %" PRIu32 "\n"
"\t ps_perspective_sample_vgpr = %" PRIu32 "\n"
"\t ps_perspective_centroid_vgpr = %" PRIu32 "\n"
"\t ps_pos_x = %s\n"
"\t ps_pos_y = %s\n"
@@ -923,7 +937,8 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
"\t ps_early_z = %s\n"
"\t ps_execute_on_noop = %s\n",
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
info.ps_perspective_center_vgpr, info.ps_perspective_centroid_vgpr,
info.ps_perspective_center_vgpr, info.ps_perspective_sample_vgpr,
info.ps_perspective_centroid_vgpr,
info.ps_pos_x ? "true" : "false",
info.ps_pos_y ? "true" : "false", info.ps_pos_z ? "true" : "false",
info.ps_pos_w ? "true" : "false", info.ps_front_face ? "true" : "false",
+3
View File
@@ -167,8 +167,10 @@ struct ShaderPixelInputInfo {
uint32_t ps_system_input_base = 0;
uint32_t custom_interpolation_mask = 0;
uint32_t ps_perspective_center_vgpr = UINT32_MAX;
uint32_t ps_perspective_sample_vgpr = UINT32_MAX;
uint32_t ps_perspective_centroid_vgpr = UINT32_MAX;
uint8_t target_output_mode[8] = {};
uint32_t target_shader_mask = UINT32_MAX;
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
uint32_t scratch_size_dwords = 0;
bool ps_pos_x = false;
@@ -309,6 +311,7 @@ struct ShaderMappedData {
void ShaderInit();
void ShaderMapUserData(uint64_t addr, const ShaderMappedData& data);
uint32_t ShaderPixelExportTarget(uint32_t shader_mask, uint32_t export_index);
void ShaderDbgDumpInputInfo(const ShaderVertexInputInfo& info);
void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info);
@@ -11,6 +11,19 @@ constexpr uint32_t PsInputFlatShade = 0x00000400u;
} // namespace
uint32_t ShaderPixelExportTarget(uint32_t shader_mask, uint32_t export_index) {
for (uint32_t target = 0; target < 8u; target++) {
if (((shader_mask >> (target * 4u)) & 0xfu) == 0u) {
continue;
}
if (export_index == 0u) {
return target;
}
export_index--;
}
return UINT32_MAX;
}
uint32_t ShaderPixelParameterMappedLocation(const ShaderPixelInputInfo& info, uint32_t input) {
return input < info.input_num ? info.interpolator_settings[input] & PsInputOffsetMask : input;
}
+109 -97
View File
@@ -21,6 +21,7 @@
#include <map>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -168,6 +169,7 @@ static bool VirtualRangesOverlap(uint64_t left_start, uint64_t left_size, uint64
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
static uint32_t g_test_backing_store_unmaps_before_failure = UINT32_MAX;
static callback_func_t g_test_before_backing_map = nullptr;
static callback_func_t g_test_backing_read = nullptr;
#endif
#include "memoryAddressSpace.inc"
@@ -359,12 +361,6 @@ public:
EditUnlocked(start, size, [protection](Range* r) { r->protection = protection; });
}
void SetMemoryType(uint64_t start, uint64_t size, int memory_type) {
Common::LockGuard lock(m_mutex);
EditUnlocked(start, size, [memory_type](Range* r) { r->memory_type = memory_type; });
}
bool Query(uint64_t addr, int flags, Range* out) {
EXIT_IF(out == nullptr);
@@ -702,7 +698,7 @@ public:
bool Release(uint64_t start, size_t len, uint64_t* vaddr, uint64_t* size,
GpuAccessMode* gpu_mode);
bool Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int prot, VirtualMemory::Mode mode,
GpuAccessMode gpu_mode);
GpuAccessMode gpu_mode, const char* name, int memory_type);
bool Unmap(uint64_t vaddr, uint64_t size, GpuAccessMode* gpu_mode,
uint64_t* host_vaddr_to_release = nullptr);
bool Find(uint64_t vaddr, uint64_t* base_addr, size_t* len, int* prot,
@@ -715,7 +711,6 @@ public:
void ProtectMapping(uint64_t vaddr, uint64_t size, int prot, VirtualMemory::Mode mode,
GpuAccessMode gpu_mode);
void SetVirtualRangeName(uint64_t vaddr, uint64_t len, const char* name);
void SetVirtualRangeMemoryType(uint64_t vaddr, uint64_t len, int memory_type);
[[nodiscard]] Common::Mutex& GetMutex() { return m_mutex; }
[[nodiscard]] const std::map<uint64_t, AllocatedBlock>& GetPhysicalBlocks() const {
@@ -1368,7 +1363,8 @@ bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64
}
bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int prot,
VirtualMemory::Mode mode, GpuAccessMode gpu_mode) {
VirtualMemory::Mode mode, GpuAccessMode gpu_mode, const char* name,
int memory_type) {
Common::LockGuard lock(m_mutex);
if (len == 0 || UINT64_MAX - phys_addr < len) {
@@ -1413,6 +1409,8 @@ bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int pro
mapping.prot = prot;
mapping.mode = mode;
mapping.gpu_mode = gpu_mode;
mapping.memory_type = memory_type;
CopyVirtualRangeName(mapping.name, name);
m_mappings.push_back(mapping);
if (has_automatic) {
RemoveFreeRange(m_automatic_free, phys_addr, len);
@@ -2040,16 +2038,6 @@ void FlexibleMemory::SetVirtualRangeName(uint64_t vaddr, uint64_t len, const cha
}
}
void PhysicalMemory::SetVirtualRangeMemoryType(uint64_t vaddr, uint64_t len, int memory_type) {
Common::LockGuard lock(m_mutex);
for (auto& b: m_mappings) {
if (VirtualRangesOverlap(vaddr, len, b.map_vaddr, b.map_size)) {
b.memory_type = memory_type;
}
}
}
uint64_t FlexibleMemory::Available() {
Common::LockGuard lock(m_mutex);
@@ -2268,12 +2256,9 @@ static bool UnmapPooledBackingTransactional(const std::vector<PooledMemory::Mapp
return true;
}
int32_t KYTY_SYSV_ABI KernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot,
int flags, const char* name) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry points and batch operations hold g_memory_operation_mutex.
static int MapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags,
const char* name) {
EXIT_NOT_IMPLEMENTED(addr_in_out == nullptr);
constexpr size_t PAGE_SIZE = 0x4000;
@@ -2398,8 +2383,17 @@ int32_t KYTY_SYSV_ABI KernelMapNamedFlexibleMemory(void** addr_in_out, size_t le
return OK;
}
int32_t KYTY_SYSV_ABI KernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot,
int flags, const char* name) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return MapFlexibleMemory(addr_in_out, len, prot, flags, name);
}
int KYTY_SYSV_ABI KernelMapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags) {
return KernelMapNamedFlexibleMemory(addr_in_out, len, prot, flags, "");
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return MapFlexibleMemory(addr_in_out, len, prot, flags, "");
}
int KYTY_SYSV_ABI KernelSetPrtAperture(int index, void* addr, size_t len) {
@@ -2607,11 +2601,8 @@ static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
return OK;
}
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry point and batch operations hold g_memory_operation_mutex.
static int UnmapMemory(uint64_t vaddr, size_t len) {
LOGF("\t start = 0x%016" PRIx64 "\n"
"\t len = 0x%016" PRIx64 "\n",
vaddr, len);
@@ -2627,6 +2618,12 @@ int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
return UnmapMemoryRange(vaddr, len);
}
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return UnmapMemory(vaddr, len);
}
size_t KYTY_SYSV_ABI KernelGetDirectMemorySize() {
PRINT_NAME();
@@ -2909,7 +2906,8 @@ static int ReleaseDirectMemoryInternal(int64_t start, size_t len) {
for (const auto& removed: metadata_unmapped) {
EXIT_IF(!g_physical_memory->Map(removed.map_vaddr, removed.start_addr,
removed.map_size, removed.prot, removed.mode,
removed.gpu_mode));
removed.gpu_mode, removed.name,
removed.memory_type));
}
restore_owner_aliases(owner_unmapped);
restore_gpu_aliases();
@@ -2980,12 +2978,10 @@ int KYTY_SYSV_ABI KernelCheckedReleaseDirectMemory(int64_t start, size_t len) {
return result == KERNEL_ERROR_EACCES ? KERNEL_ERROR_ENOENT : result;
}
int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry points and batch operations hold g_memory_operation_mutex.
static int MapDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment, const char* name,
int type = -1) {
if (addr == nullptr) {
return KERNEL_ERROR_EFAULT;
}
@@ -3036,10 +3032,8 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
old.start_addr, old.mode));
}
EXIT_IF(!g_physical_memory->Map(old.map_vaddr, old.start_addr, old.map_size,
old.prot, old.mode, old.gpu_mode));
g_physical_memory->SetVirtualRangeName(old.map_vaddr, old.map_size, old.name);
g_physical_memory->SetVirtualRangeMemoryType(old.map_vaddr, old.map_size,
old.memory_type);
old.prot, old.mode, old.gpu_mode, old.name,
old.memory_type));
}
MapGpuRange(in_addr, len);
};
@@ -3168,7 +3162,11 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
return KERNEL_ERROR_ENOMEM;
}
if (!g_physical_memory->Map(out_addr, direct_memory_start, len, prot, mode, gpu_mode)) {
PhysicalMemory::AllocatedBlock mapped_block {};
g_physical_memory->Find(direct_memory_start, false, &mapped_block);
const int memory_type = type == -1 ? mapped_block.memory_type : type;
if (!g_physical_memory->Map(out_addr, direct_memory_start, len, prot, mode, gpu_mode, name,
memory_type)) {
LOGF_COLOR(Log::Color::Red, "\t [Fail]\n");
if (replaced_mappings.empty()) {
EXIT_IF(!g_guest_address_space->UnmapBacking(out_addr, len));
@@ -3178,18 +3176,16 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
return KERNEL_ERROR_EBUSY;
}
PhysicalMemory::AllocatedBlock mapped_block {};
g_physical_memory->Find(direct_memory_start, false, &mapped_block);
const bool published = !replaced_mappings.empty() || reserved_target
? g_virtual_ranges->ReplaceSpan(
out_addr, len, replaced_mappings.empty()
? VirtualRangeType::Reserved
: VirtualRangeType::Direct,
direct_memory_start,
prot, mapped_block.memory_type, VirtualRangeType::Direct, "")
prot, memory_type, VirtualRangeType::Direct, name)
: g_virtual_ranges->Add(out_addr, len, direct_memory_start, prot,
mapped_block.memory_type,
VirtualRangeType::Direct, "");
memory_type,
VirtualRangeType::Direct, name);
if (!published) {
GpuAccessMode rollback_gpu_mode = GpuAccessMode::NoAccess;
EXIT_IF(!g_physical_memory->Unmap(out_addr, len, &rollback_gpu_mode));
@@ -3212,22 +3208,18 @@ int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int f
return OK;
}
int KYTY_SYSV_ABI KernelMapDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return MapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment, "");
}
int KYTY_SYSV_ABI KernelMapDirectMemory2(void** addr, size_t len, int type, int prot, int flags,
int64_t direct_memory_start, size_t alignment) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
LOGF("\t type = %d\n", type);
auto ret = KernelMapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment);
if (ret == OK && addr != nullptr && *addr != nullptr) {
const auto out_addr = reinterpret_cast<uint64_t>(*addr);
g_physical_memory->SetVirtualRangeMemoryType(out_addr, len, type);
g_virtual_ranges->SetMemoryType(out_addr, len, type);
}
return ret;
return MapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment, "", type);
}
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
@@ -3246,11 +3238,11 @@ int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
uint64_t mapped = 0;
for (const auto& [offset, length]: ranges) {
void* address = reinterpret_cast<void*>(vaddr + mapped);
const int result = KernelMapDirectMemory2(&address, length, type, prot, 0x10,
static_cast<int64_t>(offset), page_size);
const int result = MapDirectMemory(&address, length, prot, 0x10,
static_cast<int64_t>(offset), page_size, "", type);
if (result != OK) {
if (mapped != 0) {
EXIT_IF(KernelMunmap(vaddr, mapped) != OK);
EXIT_IF(UnmapMemory(vaddr, mapped) != OK);
}
g_physical_memory->RestoreAutomatic(ranges);
return result;
@@ -3277,13 +3269,7 @@ int KYTY_SYSV_ABI KernelMapNamedDirectMemory(void** addr, size_t len, int prot,
return KERNEL_ERROR_ENAMETOOLONG;
}
auto ret = KernelMapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment);
if (ret == OK && addr != nullptr) {
g_physical_memory->SetVirtualRangeName(reinterpret_cast<uint64_t>(*addr), len, name);
g_virtual_ranges->Rename(reinterpret_cast<uint64_t>(*addr), len, name);
}
return ret;
return MapDirectMemory(addr, len, prot, flags, direct_memory_start, alignment, name);
}
int KYTY_SYSV_ABI KernelIsAddressSanitizerEnabled() {
@@ -3389,7 +3375,8 @@ static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size) {
if (chunk.backend_unmapped) {
backend_restored = g_physical_memory->Map(
chunk.range.start, chunk.range.offset, chunk.range.size,
chunk.range.protection, chunk.mode, chunk.gpu_mode);
chunk.range.protection, chunk.mode, chunk.gpu_mode, chunk.range.name,
chunk.range.memory_type);
}
} else if (chunk.range.type == VirtualRangeType::Flexible && chunk.backend_unmapped) {
host_restored = backend_restored =
@@ -3584,6 +3571,10 @@ void TestBeforeNextBackingMap(callback_func_t callback) {
g_test_before_backing_map = callback;
}
void TestSetBackingReadCallback(callback_func_t callback) {
g_test_backing_read = callback;
}
void TestFailNextPhysicalMemoryUnmap() {
TestFailPhysicalMemoryUnmapAfter(0);
}
@@ -3621,6 +3612,36 @@ uint64_t TestGuestBackingSize() {
return g_guest_address_space->GetBackingSize();
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool TestWindowsBackingViewModes() {
constexpr uint64_t size = 0x10000;
GuestBackingStore backing(size);
void* placeholder = VirtualAlloc2(GetCurrentProcess(), nullptr, size,
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS,
nullptr, 0);
if (placeholder == nullptr) {
return false;
}
const auto address = reinterpret_cast<uint64_t>(placeholder);
bool valid = true;
for (auto [mode, protection]: std::array {
std::pair {VirtualMemory::Mode::NoAccess, DWORD {PAGE_NOACCESS}},
std::pair {VirtualMemory::Mode::Read, DWORD {PAGE_READONLY}},
std::pair {VirtualMemory::Mode::ReadWrite, DWORD {PAGE_READWRITE}},
std::pair {VirtualMemory::Mode::ExecuteReadWrite, DWORD {PAGE_EXECUTE_READWRITE}}}) {
if (!backing.MapFixed(address, size, 0, mode)) {
valid = false;
break;
}
MEMORY_BASIC_INFORMATION info {};
valid = VirtualQuery(placeholder, &info, sizeof(info)) != 0 &&
info.Type == MEM_MAPPED && info.Protect == protection && valid;
EXIT_IF(!backing.Unmap(address, size));
}
return VirtualFree(placeholder, 0, MEM_RELEASE) != 0 && valid;
}
#endif
bool TestGuestFreeRangeBounds() {
return GuestFreeRangeContains(0x10000, 0x20000, 0x18000, 0x4000) &&
!GuestFreeRangeContains(0x10000, 0x20000, 0x40000, 0x4000) &&
@@ -3875,11 +3896,8 @@ bool FreeGuestMemory(uint64_t vaddr, uint64_t size) {
return FreeGuestMemoryOwner(vaddr, mapped_size);
}
int KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
// The ABI entry points and batch operations hold g_memory_operation_mutex.
static int ProtectMemory(const void* addr, size_t len, int prot) {
auto vaddr = reinterpret_cast<uint64_t>(addr);
LOGF("\t addr = 0x%016" PRIx64 "\n"
@@ -3937,19 +3955,17 @@ int KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot) {
return OK;
}
int KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
return ProtectMemory(addr, len, prot);
}
int KYTY_SYSV_ABI KernelMtypeprotect(const void* addr, size_t len, int type, int prot) {
PRINT_NAME();
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
LOGF("\t addr = 0x%016" PRIx64 "\n"
"\t len = 0x%016" PRIx64 "\n"
"\t type = 0x%08" PRIx32 "\n"
"\t prot = 0x%08" PRIx32 "\n",
reinterpret_cast<uint64_t>(addr), static_cast<uint64_t>(len), static_cast<uint32_t>(type),
static_cast<uint32_t>(prot));
return KernelMprotect(addr, len, prot);
LOGF("\t type = %d\n", type);
return ProtectMemory(addr, len, prot);
}
int KYTY_SYSV_ABI KernelBatchMap2(KernelBatchMapEntry* entries, int num_entries,
@@ -3996,23 +4012,19 @@ int KYTY_SYSV_ABI KernelBatchMap2(KernelBatchMapEntry* entries, int num_entries,
int result = OK;
switch (entry->operation) {
case MAP_OP_MAP_DIRECT:
result = KernelMapNamedDirectMemory(&entry->start, entry->length, entry->protection,
flags, static_cast<int64_t>(entry->offset), 0,
"anon");
result = MapDirectMemory(&entry->start, entry->length, entry->protection, flags,
static_cast<int64_t>(entry->offset), 0, "anon");
break;
case MAP_OP_UNMAP:
result = KernelMunmap(reinterpret_cast<uint64_t>(entry->start), entry->length);
result = UnmapMemory(reinterpret_cast<uint64_t>(entry->start), entry->length);
break;
case MAP_OP_PROTECT:
result = KernelMprotect(entry->start, entry->length, entry->protection);
case MAP_OP_TYPE_PROTECT:
result = ProtectMemory(entry->start, entry->length, entry->protection);
break;
case MAP_OP_MAP_FLEXIBLE:
result = KernelMapNamedFlexibleMemory(&entry->start, entry->length,
entry->protection, flags, "anon");
break;
case MAP_OP_TYPE_PROTECT:
result =
KernelMtypeprotect(entry->start, entry->length, entry->type, entry->protection);
result = MapFlexibleMemory(&entry->start, entry->length, entry->protection, flags,
"anon");
break;
default: result = KERNEL_ERROR_EINVAL; break;
}
+1
View File
@@ -192,6 +192,7 @@ bool FreeGuestMemory(uint64_t vaddr, uint64_t size);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestBeforeNextBackingMap(callback_func_t callback);
void TestSetBackingReadCallback(callback_func_t callback);
void TestFailNextPhysicalMemoryUnmap();
void TestFailPhysicalMemoryUnmapAfter(uint32_t successful_unmaps);
void TestFailGuestBackingStoreUnmapAfter(uint32_t successful_unmaps);
+88 -66
View File
@@ -161,30 +161,90 @@ public:
if (!IsAvailable() || !RangeOk(backing_offset, size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::memset(m_backing_base + backing_offset, 0, size);
return true;
}
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) {
if (data == nullptr) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
return TryTransferBacking(vaddr, size,
[source](uint8_t* backing, uint64_t data_offset, size_t bytes) {
std::memcpy(backing, source + data_offset, bytes);
});
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(m_backing_base + offset, source + (current - vaddr), bytes);
current += bytes;
}
return true;
}
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) {
if (data == nullptr) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::shared_lock lock(m_mutex);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_read != nullptr) {
g_test_backing_read(vaddr, size);
}
#endif
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
auto* destination = static_cast<uint8_t*>(data);
return TryTransferBacking(
vaddr, size, [destination](uint8_t* backing, uint64_t data_offset, size_t bytes) {
std::memcpy(destination + data_offset, backing, bytes);
});
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(destination + (current - vaddr), m_backing_base + offset, bytes);
current += bytes;
}
return true;
}
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
@@ -192,29 +252,24 @@ public:
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.lower_bound(vaddr);
if (first != m_maps.begin()) {
auto previous = std::prev(first);
if (previous->second.vaddr + previous->second.size > vaddr) {
first = previous;
}
std::shared_lock lock(m_mutex);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_read != nullptr) {
g_test_backing_read(vaddr, size);
}
for (auto mapping = first; mapping != m_maps.end() && mapping->second.vaddr < end;
++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
if (!RangeOk(entry.backing_offset + start - entry.vaddr, bytes)) {
return false;
#endif
const auto end = vaddr + size;
auto mapping = m_maps.lower_bound(vaddr);
if (mapping != m_maps.begin()) {
auto previous = std::prev(mapping);
if (previous->second.vaddr + previous->second.size > vaddr) {
mapping = previous;
}
}
auto* destination = static_cast<uint8_t*>(data);
auto cursor = vaddr;
for (auto mapping = first; mapping != m_maps.end() && mapping->second.vaddr < end;
++mapping) {
for (; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
@@ -437,7 +492,7 @@ public:
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
const auto end = vaddr + size;
auto current = vaddr;
@@ -457,39 +512,6 @@ public:
}
private:
template <typename Transfer>
bool TryTransferBacking(uint64_t vaddr, uint64_t size, Transfer transfer) {
if (!IsAvailable() || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto current = vaddr;
while (current < end) {
MapEntry entry {};
if (!FindContainingUnlocked(current, 1, &entry)) {
return false;
}
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
current = vaddr;
while (current < end) {
MapEntry entry {};
EXIT_IF(!FindContainingUnlocked(current, 1, &entry));
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
transfer(m_backing_base + offset, current - vaddr, static_cast<size_t>(bytes));
current += bytes;
}
return true;
}
struct MapEntry {
uint64_t vaddr = 0;
uint64_t size = 0;
@@ -597,7 +619,7 @@ private:
return nullptr;
}
if (!ApplyProtection(ptr, size, mode)) {
if (mode == VirtualMemory::Mode::NoAccess && !ApplyProtection(ptr, size, mode)) {
UnmapViewOfFile2(GetCurrentProcess(), ptr, MEM_PRESERVE_PLACEHOLDER);
SetFailure(failure_reason, FailureReason::ProtectFailed);
return nullptr;
@@ -656,7 +678,7 @@ private:
uint8_t* m_backing_base = nullptr;
uint64_t m_size = 0;
std::mutex m_mutex;
std::shared_mutex m_mutex;
std::map<uint64_t, MapEntry> m_maps;
};
@@ -727,7 +749,7 @@ public:
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) {
return vaddr >= range.first && vaddr + size <= range.first + range.second;
});
@@ -755,7 +777,7 @@ public:
if (!Owns(vaddr, size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
@@ -1381,7 +1403,7 @@ private:
#endif
}
std::mutex m_mutex;
std::shared_mutex m_mutex;
std::map<uint64_t, uint64_t> m_free;
std::map<uint64_t, MappedRegion> m_mapped;
std::vector<std::pair<uint64_t, uint64_t>> m_owned;
@@ -179,6 +179,18 @@
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
</layout>
</widget>
</item>
+4
View File
@@ -119,8 +119,10 @@ public:
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool skip_notice_screen = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -152,8 +154,10 @@ public:
fullscreen_enabled = other.fullscreen_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
skip_notice_screen = other.skip_notice_screen;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
+4
View File
@@ -12,8 +12,10 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(sync_raw_image_buffers);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(skip_notice_screen);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -55,8 +57,10 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(sync_raw_image_buffers);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
KYTY_CFG_GET(skip_notice_screen);
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
console_language = s.value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -275,7 +275,9 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@@ -434,7 +436,9 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
+2
View File
@@ -245,7 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--hide-cursor";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
if (info.tessellation_enabled) {
args << "--tessellation";
}
+130 -39
View File
@@ -9,6 +9,7 @@
#include <QDateTime>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
@@ -18,6 +19,7 @@
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QPushButton>
#include <QRegularExpression>
#include <QSize>
#include <QStringList>
@@ -25,6 +27,8 @@
#include <QTabWidget>
#include <QVBoxLayout>
#include <utility>
namespace {
static const QRegularExpression TrophyFilePattern(QStringLiteral("^trophy(\\d+)\\.ucp$"));
@@ -54,17 +58,115 @@ static QString GradeToText(int grade) {
}
}
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
QStringList lines {QString::fromStdString(trophy.name),
QString::fromStdString(trophy.description),
QObject::tr("Grade: %1").arg(GradeToText(trophy.grade))};
if (trophy.hidden) {
lines.append(QObject::tr("Hidden trophy"));
QString TrophyName(int id, const Common::Trophies::Trophy& trophy, bool masked) {
return masked ? QObject::tr("Hidden trophy")
: trophy.name.empty() ? QObject::tr("Trophy %1").arg(id)
: QString::fromStdString(trophy.name);
}
QPixmap TrophyIcon(const Common::Trophies::Trophy& trophy, bool unlocked, bool masked, int size) {
QPixmap pixmap;
if (masked) {
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size())) &&
!unlocked) {
auto image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
for (int y = 0; y < image.height(); ++y) {
auto* line = reinterpret_cast<QRgb*>(image.scanLine(y));
for (int x = 0; x < image.width(); ++x) {
const int gray = qGray(line[x]);
line[x] = qRgba(gray, gray, gray, qAlpha(line[x]));
}
}
pixmap = QPixmap::fromImage(image);
}
if (trophy.has_reward && !trophy.reward.empty()) {
lines.append(QObject::tr("Reward: %1").arg(QString::fromStdString(trophy.reward)));
return pixmap.isNull()
? pixmap
: pixmap.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
QString TrophyEarnedDate(int id, const Common::Trophies::UnlockData& unlocks) {
const auto found = unlocks.timestamps.find(id);
if (found == unlocks.timestamps.end()) {
return {};
}
return lines.join(QLatin1Char('\n'));
return QDateTime::fromMSecsSinceEpoch(
static_cast<qint64>((found->second - Common::Trophies::UnixEpochTick) / 1000))
.toLocalTime()
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
}
void ShowTrophyInspector(QWidget* parent, int id, const Common::Trophies::Trophy& trophy,
bool unlocked, const QString& earned_date) {
QDialog dialog(parent);
dialog.setWindowTitle(QObject::tr("Trophy Details"));
dialog.resize(640, 380);
auto* layout = new QVBoxLayout(&dialog);
auto* top = new QHBoxLayout;
auto* icon = new QLabel(&dialog);
icon->setFixedSize(148, 148);
icon->setAlignment(Qt::AlignCenter);
auto* text_box = new QVBoxLayout;
auto* name = new QLabel(&dialog);
name->setTextFormat(Qt::PlainText);
name->setStyleSheet(QStringLiteral("font-size: 26px;"));
name->setWordWrap(true);
auto* subtitle = new QLabel(QObject::tr("Hidden trophy"), &dialog);
subtitle->setStyleSheet(QStringLiteral("font-size: 16px;"));
auto* reveal = new QPushButton(QObject::tr("Show Hidden Trophy"), &dialog);
text_box->addWidget(name);
text_box->addWidget(subtitle);
text_box->addWidget(reveal, 0, Qt::AlignLeft);
text_box->addStretch(1);
top->addWidget(icon);
top->addSpacing(16);
top->addLayout(text_box, 1);
layout->addLayout(top);
auto* grid = new QGridLayout;
grid->setColumnStretch(1, 1);
const auto make_row = [&](const QString& label, const QString& text = {}) {
auto* key = new QLabel(label, &dialog);
auto* value = new QLabel(text, &dialog);
key->setStyleSheet(QStringLiteral("font-size: 17px;"));
value->setStyleSheet(QStringLiteral("font-size: 17px;"));
value->setTextFormat(Qt::PlainText);
value->setWordWrap(true);
const int row = grid->rowCount();
grid->addWidget(key, row, 0);
grid->addWidget(value, row, 1);
return value;
};
auto* grade = make_row(QObject::tr("Grade"));
make_row(QObject::tr("Status"), unlocked ? QObject::tr("Earned") : QObject::tr("Not earned"));
make_row(QObject::tr("Earned date"),
unlocked && !earned_date.isEmpty() ? earned_date : QStringLiteral("-"));
auto* details = make_row(QObject::tr("Details"));
auto* reward =
trophy.has_reward && !trophy.reward.empty() ? make_row(QObject::tr("Reward")) : nullptr;
layout->addLayout(grid);
layout->addStretch(1);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttons);
const auto refresh = [&](bool masked) {
icon->setPixmap(TrophyIcon(trophy, unlocked, masked, 148));
name->setText(TrophyName(id, trophy, masked));
subtitle->setVisible(trophy.hidden && !masked);
reveal->setVisible(masked);
grade->setText(masked ? QObject::tr("Hidden trophy") : GradeToText(trophy.grade));
details->setText(masked || trophy.description.empty()
? QStringLiteral("-")
: QString::fromStdString(trophy.description));
if (reward != nullptr) {
reward->setText(masked ? QStringLiteral("-") : QString::fromStdString(trophy.reward));
}
};
QObject::connect(reveal, &QPushButton::clicked, &dialog, [&] { refresh(false); });
refresh(trophy.hidden && !unlocked);
dialog.exec();
}
} // namespace
@@ -337,13 +439,13 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
}
QStringList errors;
for (const auto& file: trophy_files) {
const auto package =
auto package =
Common::Trophies::LoadPackage(GameContent::ToPath(file), info.console_language);
if (package.trophies.empty()) {
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
continue;
}
const auto unlocks = LoadUnlocks(info, runtime_directory, file);
auto unlocks = LoadUnlocks(info, runtime_directory, file);
const auto counts = Common::Trophies::GetProgress(package, unlocks);
auto* page = new QWidget(m_tabs);
auto* page_layout = new QVBoxLayout(page);
@@ -395,8 +497,10 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
const auto hidden_locked = trophy.hidden && !trophy_unlocked;
auto* item = new QListWidgetItem(list);
item->setSizeHint(QSize(900, 96));
item->setData(Qt::UserRole, id);
auto* card = new QWidget(list);
card->setAttribute(Qt::WA_TransparentForMouseEvents);
card->setStyleSheet(
QStringLiteral("QWidget#card { background: palette(base); border-radius: 12px; }"));
card->setObjectName(QStringLiteral("card"));
@@ -404,26 +508,12 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
auto* icon = new QLabel(card);
icon->setFixedSize(76, 76);
icon->setAlignment(Qt::AlignCenter);
QPixmap pixmap;
if (hidden_locked) {
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size())) &&
!trophy_unlocked) {
pixmap =
QPixmap::fromImage(pixmap.toImage().convertToFormat(QImage::Format_Grayscale8));
}
if (!pixmap.isNull()) {
icon->setPixmap(
pixmap.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
icon->setPixmap(TrophyIcon(trophy, trophy_unlocked, hidden_locked, 76));
row->addWidget(icon);
auto* left = new QVBoxLayout;
const auto name_text = hidden_locked ? tr("Hidden trophy")
: trophy.name.empty() ? tr("Trophy %1").arg(id)
: QString::fromStdString(trophy.name);
auto* name = new QLabel(name_text, card);
auto* left = new QVBoxLayout;
auto* name = new QLabel(TrophyName(id, trophy, hidden_locked), card);
name->setTextFormat(Qt::PlainText);
name->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: bold;"));
left->addWidget(name);
if (!hidden_locked) {
@@ -440,15 +530,9 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
row->addLayout(left, 2);
auto* right = new QVBoxLayout;
QString date_text;
if (const auto found = unlocks.timestamps.find(id); found != unlocks.timestamps.end()) {
date_text = QDateTime::fromMSecsSinceEpoch(
static_cast<qint64>(
(found->second - Common::Trophies::UnixEpochTick) / 1000))
.toLocalTime()
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
} else if (trophy_unlocked) {
auto* right = new QVBoxLayout;
auto date_text = TrophyEarnedDate(id, unlocks);
if (date_text.isEmpty() && trophy_unlocked) {
date_text = tr("Earned");
}
auto* date = new QLabel(date_text, card);
@@ -456,15 +540,22 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
date->setStyleSheet(QStringLiteral("font-size: 15px;"));
auto* detail = new QLabel(
hidden_locked ? QString {} : QString::fromStdString(trophy.description), card);
detail->setTextFormat(Qt::PlainText);
detail->setWordWrap(true);
detail->setAlignment(Qt::AlignLeft | Qt::AlignTop);
detail->setStyleSheet(QStringLiteral("font-size: 17px;"));
right->addWidget(date);
right->addWidget(detail, 1);
row->addLayout(right, 3);
card->setToolTip(hidden_locked ? name_text : TrophyTooltip(trophy));
list->setItemWidget(item, card);
}
connect(list, &QListWidget::itemClicked, this,
[this, trophies = std::move(package.trophies),
unlocks = std::move(unlocks)](QListWidgetItem* item) {
const int id = item->data(Qt::UserRole).toInt();
ShowTrophyInspector(this, id, trophies.at(id), unlocks.unlocked.contains(id),
TrophyEarnedDate(id, unlocks));
});
m_tabs->addTab(page, QString::fromStdString(package.title));
}
m_tabs->tabBar()->setVisible(m_tabs->count() > 1);
+6
View File
@@ -2388,6 +2388,12 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize() {
PRINT_NAME();
return 5u * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs) {
+1
View File
@@ -137,6 +137,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize();
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
+25 -29
View File
@@ -15,7 +15,6 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <magic_enum.hpp>
#include <vector>
@@ -90,8 +89,9 @@ public:
};
struct OutputParam {
Id handle;
const void* data = nullptr;
Id handle;
const void* data = nullptr;
const float* gains = nullptr;
};
Audio() = default;
@@ -159,8 +159,10 @@ private:
static void OpenSdlDevice(PortIn* port, Format format);
static void CloseSdlDevice(PortIn* port);
static const void* PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer, float gain);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain);
std::vector<uint8_t>* buffer, float gain,
const float* gains);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain,
const float* gains);
};
static Audio* g_audio = nullptr;
@@ -196,7 +198,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
for (uint32_t i = 0; i < num; i++) {
if (params[i].handle > 0 && params[i].data != nullptr) {
output_params.push_back(
Audio::OutputParam {Audio::Id(params[i].handle), params[i].data});
Audio::OutputParam {Audio::Id(params[i].handle), params[i].data, params[i].gains});
}
}
@@ -302,7 +304,8 @@ void Audio::CloseSdlDevice(PortOut* port) {
}
const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer, float gain) {
std::vector<uint8_t>* buffer, float gain,
const float* gains) {
EXIT_IF(data == nullptr);
EXIT_IF(buffer == nullptr);
@@ -312,12 +315,12 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
const auto bytes_per_sample = BytesPerSample(port.format);
const bool reorder = channels >= 8 && !FormatIsStd(port.format);
bool volume_changed = gain != 1.0f;
std::array<double, 12> scales {};
bool volume_changed = false;
for (uint32_t ch = 0; ch < channels; ch++) {
if (port.volume[ch] != 32768) {
volume_changed = true;
break;
}
scales[ch] = (static_cast<double>(port.volume[ch]) / 32768.0) * gain *
(gains != nullptr ? gains[ch] : 1.0f);
volume_changed |= scales[ch] != 1.0;
}
if (!volume_changed && !reorder) {
@@ -337,8 +340,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t ch = 0; ch < output_channels; ch++) {
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
dst[frame * output_channels + ch] =
src[frame * channels + src_ch] *
(static_cast<float>(port.volume[src_ch]) / 32768.0f) * gain;
static_cast<float>(src[frame * channels + src_ch] * scales[src_ch]);
}
if (channels == 12) {
// Add the four top channels to their front/back channels, turning 12 into 8.
@@ -346,8 +348,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
static constexpr uint32_t HEIGHT_DST[4] = {0, 1, 4, 5};
for (uint32_t ch = 0; ch < 4; ch++) {
dst[frame * output_channels + HEIGHT_DST[ch]] +=
src[frame * channels + 8 + ch] *
(static_cast<float>(port.volume[8 + ch]) / 32768.0f) * gain;
static_cast<float>(src[frame * channels + 8 + ch] * scales[8 + ch]);
}
}
}
@@ -358,15 +359,9 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < output_channels; ch++) {
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
int64_t sample =
static_cast<int64_t>(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768;
sample = static_cast<int64_t>(sample * static_cast<double>(gain));
if (sample > std::numeric_limits<int16_t>::max()) {
sample = std::numeric_limits<int16_t>::max();
} else if (sample < std::numeric_limits<int16_t>::min()) {
sample = std::numeric_limits<int16_t>::min();
}
dst[frame * output_channels + ch] = static_cast<int16_t>(sample);
const double sample = src[frame * channels + src_ch] * scales[src_ch];
dst[frame * output_channels + ch] = static_cast<int16_t>(
std::clamp(sample, -32768.0, 32767.0));
}
}
}
@@ -374,7 +369,8 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
return buffer->data();
}
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) {
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain,
const float* gains) {
EXIT_IF(port == nullptr);
if (port->stream == nullptr || data == nullptr) {
@@ -382,7 +378,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float
}
std::vector<uint8_t> prepared_buffer;
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain);
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain, gains);
const auto output_channels = OutputChannels(*port);
const auto prepared_size =
BytesPerSample(port->format) * output_channels * port->samples_num;
@@ -570,14 +566,14 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
controller_queued_us = Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume,
gain);
gain, params[i].gains);
controller_uses_bluetooth =
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
}
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
// output instead, and a vibration port has none.
if (QueueSdlAudio(&port, params[i].data, blocking, gain)) {
if (QueueSdlAudio(&port, params[i].data, blocking, gain, params[i].gains)) {
any_device = true;
paced[i] = port.queue_primed;
}
+3 -2
View File
@@ -19,8 +19,9 @@ enum class Format {
};
struct OutputParam {
int handle = 0;
const void* data = nullptr;
int handle = 0;
const void* data = nullptr;
const float* gains = nullptr;
};
static constexpr int OUT_PORTS_MAX = 32;
+2 -2
View File
@@ -315,7 +315,7 @@ bool UsesBluetooth(const Stream* stream) {
}
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume, float gain) {
bool is_float, const int* volume, float gain, const float* gains) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return 0;
}
@@ -357,7 +357,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
float value = is_float ? static_cast<const float*>(data)[index]
: static_cast<const int16_t*>(data)[index] / 32768.0f;
value *= volume[src_ch] / 32768.0f * gain;
value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f);
// USB places the speaker in front and the actuators in back; Bluetooth takes stereo.
stream->buffer[static_cast<size_t>(frame) * output_channels + ch + channel_offset] =
value;
+2 -1
View File
@@ -17,7 +17,8 @@ bool UsesBluetooth(const Stream* stream);
// (the active pad has no usable controller audio device or its speaker cannot be routed).
// Ambiguous USB or Bluetooth endpoints are not selected.
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume, float gain = 1.0f);
bool is_float, const int* volume, float gain = 1.0f,
const float* gains = nullptr);
// Returns false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor,
uint32_t duration_ms = 0xffff);
+1
View File
@@ -95,6 +95,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("w4-d0n60hdo", Gen5::AgcDcbSetUcRegisterDirect);
LIB_FUNC("ZvwO9euwYzc", Gen5::AgcDcbSetCxRegistersIndirect);
LIB_FUNC("-HOOCn0JY48", Gen5::AgcDcbSetShRegistersIndirect);
LIB_FUNC("nNlUtdDDvZ0", Gen5::AgcDcbSetShRegistersIndirectGetSize);
LIB_FUNC("hvUfkUIQcOE", Gen5::AgcDcbSetUcRegistersIndirect);
LIB_FUNC("GIIW2J37e70", Gen5::AgcDcbSetIndexSize);
LIB_FUNC("l4fM9K-Lyks", Gen5::AgcDcbSetIndexBuffer);
+9 -25
View File
@@ -52,6 +52,7 @@ struct ResolvedPathInfo {
static std::mutex g_mutex;
static uint32_t g_next_submission_id = 1;
static uint32_t g_next_file_id = 1;
static std::unordered_map<uint32_t, bool> g_submissions;
static std::condition_variable g_submission_cv;
static std::unordered_map<uint32_t, std::string> g_files;
@@ -92,15 +93,6 @@ static bool WriteGuest(uint64_t addr, const T& value) {
return WriteGuestBytes(addr, &value, sizeof(T));
}
static uint32_t ComputeFileId(const char* guest_path) {
uint32_t hash = 2166136261u;
for (auto* p = reinterpret_cast<const uint8_t*>(guest_path); p != nullptr && *p != 0; ++p) {
hash ^= *p;
hash *= 16777619u;
}
return hash & static_cast<uint32_t>(std::numeric_limits<int32_t>::max());
}
static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
if (addr == 0) {
return LibKernel::KERNEL_ERROR_EFAULT;
@@ -120,18 +112,6 @@ static int ReadGuestCString(uint64_t addr, char* out, size_t out_size) {
return LibKernel::KERNEL_ERROR_ENAMETOOLONG;
}
static void RegisterHostPathLocked(uint32_t file_id, const std::string& host_path,
uint64_t file_size, bool is_dir) {
g_files[file_id] = host_path;
g_file_sizes[file_id] = is_dir ? 0 : file_size;
}
static void RegisterHostPath(uint32_t file_id, const std::string& host_path, uint64_t file_size,
bool is_dir) {
std::scoped_lock lock(g_mutex);
RegisterHostPathLocked(file_id, host_path, file_size, is_dir);
}
static bool TryGetHostPath(uint32_t file_id, std::string* out) {
std::scoped_lock lock(g_mutex);
const auto it = g_files.find(file_id);
@@ -213,7 +193,6 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!found) {
const auto real_path = LibKernel::FileSystem::GetRealFilename(path);
info.file_id = AprShared::ComputeFileId(guest_path);
info.host_path = Common::PathToString(real_path);
if (Common::File::IsDirectoryExisting(real_path)) {
@@ -232,7 +211,14 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (!inserted) {
info = it->second;
} else if (info.result == OK) {
RegisterHostPathLocked(info.file_id, info.host_path, info.file_size, info.is_dir);
// Allocate once under the cache lock; distinct paths can share a hash.
if (g_next_file_id == 0xffffffffu) {
info.result = it->second.result = LibKernel::KERNEL_ERROR_ENFILE;
} else {
info.file_id = it->second.file_id = g_next_file_id++;
g_files.emplace(info.file_id, info.host_path);
g_file_sizes.emplace(info.file_id, info.is_dir ? 0 : info.file_size);
}
} else if (info.result == LibKernel::KERNEL_ERROR_ENOENT) {
log_missing = true;
}
@@ -240,8 +226,6 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
if (log_missing) {
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, info.host_path.c_str());
}
} else if (info.result == OK) {
AprShared::RegisterHostPath(info.file_id, info.host_path, info.file_size, info.is_dir);
}
if (info.result != OK) {
+219 -100
View File
@@ -11,6 +11,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <vector>
@@ -170,12 +171,14 @@ static std::atomic_uint64_t g_audioout2_next_port {1};
static AudioOut2UserHandle g_audioout2_next_user = 1;
struct AudioOut2ContextState {
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
bool used = false;
AudioOut2ContextHandle handle = 0;
uint32_t queue_depth = 4;
uint32_t queued = 0;
uint32_t num_grains = 512;
uint64_t last_update = 0;
int object_audio_handle = 0;
std::vector<float> object_mix;
};
struct AudioOut2PortStateEntry {
@@ -189,9 +192,11 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
std::array<float, 16> gains {};
uint32_t ambisonics = UINT32_MAX;
bool attributes_locked = false;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
bool used = false;
uint32_t num_speakers = 0;
@@ -207,9 +212,8 @@ struct AudioOut2LatencyState {
uint32_t latency_us = 0;
};
static Common::Mutex g_audioout2_context_mutex;
static Common::Mutex g_audioout2_state_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static Common::Mutex g_audioout2_port_mutex;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
@@ -223,7 +227,11 @@ static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511;
static constexpr int AUDIO_OUT2_ERROR_BUSY = -2144960505; /* 0x80268007 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = -2144959999; /* 0x80268201 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = -2144959996; /* 0x80268204 */
static constexpr int AUDIO_OUT2_ERROR_OUT_OF_RESOURCE = -2144960510; /* 0x80268002 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PORT = -2144960503; /* 0x80268009 */
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM = 0;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN = 1;
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS = 8;
static constexpr uint32_t AUDIO_OUT2_MASTERING_OUTPUT_RECORDING = 2;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1;
static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2;
@@ -350,37 +358,80 @@ static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
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);
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
const int handle = audioout2_port_type_is_object(state.port_type)
? context.object_audio_handle
: state.audio_handle;
if (AudioInternal::AudioOutHasDevice(handle)) {
return true;
}
}
}
return false;
}
static void audioout2_mix_object(AudioOut2ContextState& context,
const AudioOut2PortStateEntry& object) {
auto& mix = context.object_mix;
if (mix.empty()) {
mix.resize(static_cast<size_t>(context.num_grains) * 2, 0.0f);
}
// Center ordinary objects. First-order stereo ambisonics uses left/right-facing
// cardioids; FuMa W has a different normalization from ACN/SN3D W.
constexpr float half_power = 0.70710678f;
float left = half_power, right = half_power;
switch (object.ambisonics) {
case UINT32_MAX:
case 64: break;
case 0: left = right = 1.0f; break;
case 2:
case 65: right = -half_power; break;
default: return;
}
left *= object.gains[0];
right *= object.gains[0];
for (uint32_t frame = 0; frame < context.num_grains; frame++) {
const float sample =
object.audio_format == AudioInternal::Format::FloatMono
? reinterpret_cast<const float*>(object.pcm_data.data())[frame]
: reinterpret_cast<const int16_t*>(object.pcm_data.data())[frame] / 32768.0f;
mix[frame * 2] += sample * left;
mix[frame * 2 + 1] += sample * right;
}
}
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.
Common::LockGuard lock(g_audioout2_state_mutex);
auto* context = audioout2_find_context_locked(ctx);
if (context == nullptr) {
return;
}
context->object_mix.clear();
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
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.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
continue;
}
if (audioout2_port_type_is_object(state.port_type)) {
audioout2_mix_object(*context, state);
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({state.audio_handle, state.pcm_data.data(), state.gains.data()});
}
}
if (!context->object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({context->object_audio_handle, context->object_mix.data()});
}
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
}
static void audioout2_close_audio_handle(int audio_handle) {
if (audio_handle > 0) {
AudioInternal::AudioOutClose(audio_handle);
@@ -431,52 +482,58 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
g_audioout2_context_mutex.Lock();
auto* state = audioout2_find_context_locked(0);
if (state == nullptr) {
for (auto& candidate: g_audioout2_contexts) {
if (!candidate.used) {
state = &candidate;
break;
}
}
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
const int object_audio_handle =
params->max_object_ports == 0
? 0
: AudioInternal::AudioOutOpen(AUDIO_OUT_PORT_TYPE_MAIN, num_grains, 48000,
AudioInternal::Format::FloatStereo);
if (params->max_object_ports != 0 && object_audio_handle == 0) {
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
EXIT_NOT_IMPLEMENTED(state == nullptr);
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
state->queued = 0;
state->num_grains = (params->num_grains == 0 ? 512u : params->num_grains);
state->last_update = LibKernel::KernelGetProcessTime();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Lock();
auto it = std::find_if(g_audioout2_contexts.begin(), g_audioout2_contexts.end(),
[](const auto& state) { return !state.used; });
if (it == g_audioout2_contexts.end()) {
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(object_audio_handle);
return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE;
}
*ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed);
auto* state = &*it;
*state = AudioOut2ContextState {};
state->used = true;
state->handle = *ctx;
state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth;
state->num_grains = num_grains;
state->last_update = LibKernel::KernelGetProcessTime();
state->object_audio_handle = object_audio_handle;
g_audioout2_state_mutex.Unlock();
LOGF("\t buffer = 0x%016" PRIx64 "\n"
"\t buffer_size = 0x%016" PRIx64 "\n"
"\t ctx = 0x%016" PRIx64 "\n"
"\t queue_depth = %" PRIu32 ", num_grains = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(buffer), static_cast<uint64_t>(buffer_size), *ctx,
state->queue_depth, state->num_grains);
(params->queue_depth == 0 ? 4u : params->queue_depth), num_grains);
return OK;
}
int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
g_audioout2_context_mutex.Lock();
std::array<int, 257> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
if (state->object_audio_handle > 0) {
audio_handles[audio_handles_num++] = state->object_audio_handle;
}
*state = AudioOut2ContextState {};
}
g_audioout2_context_mutex.Unlock();
std::array<int, 256> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_port_mutex.Lock();
for (auto& port_state: g_audioout2_ports) {
if (port_state.used && port_state.context == ctx) {
if (port_state.audio_handle > 0 && audio_handles_num < audio_handles.size()) {
@@ -485,7 +542,7 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
port_state = AudioOut2PortStateEntry {};
}
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
for (size_t i = 0; i < audio_handles_num; i++) {
audioout2_close_audio_handle(audio_handles[i]);
@@ -504,11 +561,16 @@ int KYTY_SYSV_ABI AudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx,
}
int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
}
g_audioout2_context_mutex.Unlock();
for (auto& port: g_audioout2_ports) {
if (port.used && port.context == ctx) {
port.attributes_locked = false;
}
}
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -520,11 +582,10 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
// vibration/failed-open handle) has no downstream operation that can block this call.
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(ctx);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(*state);
audioout2_update_context_locked(state);
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) {
@@ -534,12 +595,12 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
if (state->queued < state->queue_depth) {
state->queued++;
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_queue_context_audio(ctx, blocking != 0);
return OK;
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (blocking == 0) {
return AUDIO_OUT2_ERROR_NOT_READY;
@@ -558,7 +619,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
*available_queues = 4;
}
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
if (queue_level != nullptr) {
@@ -569,7 +630,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint
(state->queued < state->queue_depth ? state->queue_depth - state->queued : 0);
}
}
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return OK;
}
@@ -583,19 +644,25 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
const auto audio_format = audioout2_data_format_to_audio_format(params->data_format);
const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type);
g_audioout2_context_mutex.Lock();
g_audioout2_state_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains;
if (audioout2_port_type_is_object(params->port_type) &&
(params->port_type != 0x100 || context_state->object_audio_handle == 0 ||
params->sampling_freq != 48000 ||
(audio_format != AudioInternal::Format::FloatMono &&
audio_format != AudioInternal::Format::Signed16bitMono))) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto samples_num = context_state->num_grains;
g_audioout2_port_mutex.Lock();
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), params->user_handle) ==
g_audioout2_users.end()) {
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
@@ -616,9 +683,9 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
port_state->sampling_freq = params->sampling_freq;
port_state->samples_num = samples_num;
port_state->audio_format = audio_format;
port_state->gains.fill(1.0f);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (port_state == nullptr) {
return AUDIO_OUT2_ERROR_PORT_FULL;
@@ -632,12 +699,12 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
audio_format);
}
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
const bool reserved = port_state->used && port_state->handle == next_port;
if (reserved) {
port_state->audio_handle = audio_handle;
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
if (!reserved) {
audioout2_close_audio_handle(audio_handle);
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -663,12 +730,12 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
LOGF("\t port = 0x%016" PRIx64 "\n", port);
int audio_handle = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
audio_handle = state->audio_handle;
*state = AudioOut2PortStateEntry {};
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
audioout2_close_audio_handle(audio_handle);
@@ -677,36 +744,86 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) {
int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
const AudioOut2Attribute* attributes, uint32_t num) {
EXIT_NOT_IMPLEMENTED(num != 0 && attributes == nullptr);
if (num != 0 && attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_state_mutex);
auto* state = audioout2_find_port_locked(port);
if (state == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PORT;
}
const void* pcm_data = nullptr;
bool has_pcm = false;
const void* pcm_data = nullptr;
auto gains = state->gains;
auto ambisonics = state->ambisonics;
uint32_t seen = 0;
for (uint32_t i = 0; i < num; i++) {
if (attributes[i].attribute_id == AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM &&
attributes[i].value != nullptr && attributes[i].value_size >= sizeof(AudioOut2Pcm)) {
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attributes[i].value, sizeof(AudioOut2Pcm));
pcm_data = pcm.data;
has_pcm = true;
const auto& attribute = attributes[i];
const auto id = attribute.attribute_id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN &&
id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS) {
continue;
}
}
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
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();
if ((seen & (1u << id)) != 0) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
seen |= 1u << id;
if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && state->attributes_locked) {
continue;
}
if (attribute.value == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
switch (id) {
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM: {
if (attribute.value_size < sizeof(AudioOut2Pcm)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2Pcm pcm {};
std::memcpy(&pcm, attribute.value, sizeof(pcm));
pcm_data = pcm.data;
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN: {
const auto channels = audioout2_data_format_channels(state->data_format);
if (attribute.value_size != channels * sizeof(float)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(gains.data(), attribute.value, attribute.value_size);
for (uint32_t ch = 0; ch < channels; ch++) {
if (!std::isfinite(gains[ch]) || gains[ch] < 0.0f) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
}
break;
}
case AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS:
if (!audioout2_port_type_is_object(state->port_type) ||
attribute.value_size != sizeof(ambisonics)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
std::memcpy(&ambisonics, attribute.value, sizeof(ambisonics));
if (ambisonics != UINT32_MAX && ambisonics > 15 &&
(ambisonics < 64 || ambisonics > 99)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
break;
}
g_audioout2_port_mutex.Unlock();
}
state->gains = gains;
state->ambisonics = ambisonics;
if ((seen & (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM)) != 0) {
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();
}
state->attributes_locked = audioout2_port_type_is_object(state->port_type);
}
return OK;
}
int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) {
PRINT_NAME();
@@ -719,11 +836,11 @@ int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortS
state->reroute_counter = 0;
state->flags = 0;
g_audioout2_port_mutex.Lock();
g_audioout2_state_mutex.Lock();
if (auto* port_state = audioout2_find_port_locked(port); port_state != nullptr) {
state->num_channels = audioout2_data_format_channels(port_state->data_format);
}
g_audioout2_port_mutex.Unlock();
g_audioout2_state_mutex.Unlock();
// LOGF("\t port = 0x%016" PRIx64 "\n", port);
// LOGF("\t num_channels = %" PRIu8 "\n", state->num_channels);
@@ -743,7 +860,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
if (handle == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
*handle = g_audioout2_next_user++;
g_audioout2_users.push_back(*handle);
LOGF("\t user_id = %" PRIu32 ", handle = 0x%016" PRIx64 "\n", user_id,
@@ -754,7 +871,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) {
PRINT_NAME();
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
if (it == g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
@@ -773,13 +890,15 @@ int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
Common::LockGuard lock(g_audioout2_state_mutex);
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle) ==
g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
*context_attributes = 0;
*port_attributes = 1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM;
*port_attributes = (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN) |
(1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS);
return OK;
}
+1 -1
View File
@@ -206,7 +206,7 @@ static int KYTY_SYSV_ABI SystemServiceGetNoticeScreenSkipFlag(bool* value) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
*value = false;
*value = Config::SkipNoticeScreen();
return OK;
}
+15
View File
@@ -84,9 +84,14 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(
" --sync-raw-image-buffers <true|false> Synchronize raw reads of GPU images. Default: false.\n");
::printf(
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
::printf(
" --skip-notice-screen <true|false> Skip startup logos and notices in supported games.\n"
" Default: false.\n");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
::printf(" --redzone Protect the guest SysV red zone.\n");
#endif
@@ -423,11 +428,21 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--sync-raw-image-buffers") {
if (!ParseBool(value, options.config.sync_raw_image_buffers)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--trophy-notifications") {
if (!ParseBool(value, options.config.trophy_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--skip-notice-screen") {
if (!ParseBool(value, options.config.skip_notice_screen)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
+187 -3
View File
@@ -3,12 +3,15 @@
#include "libs/errno.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <mutex>
#include <thread>
#include <vector>
@@ -24,6 +27,7 @@ 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;
std::vector<std::array<float, 2>> g_output_gains;
size_t g_capture_bytes = 0;
int g_next_device = 1;
int g_open_waiters = 0;
@@ -102,8 +106,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) {
ContextParam param {};
param.max_object_ports = max_object_ports;
param.queue_depth = queue_depth;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
@@ -112,6 +117,12 @@ AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
return context;
}
AudioOut2::AudioOut2PortHandle CreatePort(AudioOut2::AudioOut2ContextHandle context, const PortParam& param) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK, "port create failed");
return port;
}
void TestUserSupportedAttributes() {
constexpr int invalid_param = static_cast<int32_t>(0x80268001u);
constexpr int busy = static_cast<int32_t>(0x80268007u);
@@ -123,8 +134,8 @@ void TestUserSupportedAttributes() {
uint32_t port_attributes = UINT32_MAX;
Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, &context_attributes,
&port_attributes) == OK &&
context_attributes == 0 && port_attributes == 1,
"user capabilities do not match implemented PCM support");
context_attributes == 0 && port_attributes == 0x103,
"user capabilities do not match implemented attributes");
context_attributes = port_attributes = UINT32_MAX;
Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, nullptr, &port_attributes) ==
@@ -191,6 +202,13 @@ void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) {
"setting PCM failed");
}
template <typename T>
void SetAttribute(AudioOut2::AudioOut2PortHandle port, uint32_t id, const T& value) {
const Attribute attribute {id, 0, &value, sizeof(value)};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK,
"setting attribute failed");
}
void ResetOutputCalls() {
std::lock_guard lock(g_device_mutex);
g_output_blocking.clear();
@@ -212,6 +230,23 @@ std::vector<std::vector<uint8_t>> OutputPcm() {
return g_output_pcm;
}
std::vector<std::vector<uint8_t>> PushPcm(AudioOut2::AudioOut2ContextHandle context, size_t bytes) {
CaptureOutputPcm(bytes);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "PCM push failed");
auto output = OutputPcm();
CaptureOutputPcm(0);
return output;
}
void CheckSamples(const std::vector<uint8_t>& pcm, std::initializer_list<float> frame, const char* message) {
Check(!pcm.empty() && pcm.size() % (frame.size() * sizeof(float)) == 0, "incorrect PCM size");
for (size_t i = 0; i < pcm.size() / sizeof(float); i++) {
float sample;
std::memcpy(&sample, pcm.data() + i * sizeof(float), sizeof(float));
Check(std::abs(sample - *(frame.begin() + i % frame.size())) < 0.000001f, message);
}
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -428,6 +463,148 @@ void TestPcmCopiedBeforeScratchBufferReuse() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPortGainAndValidation() {
const auto context = CreateContext();
const auto port = CreatePort(context, MakeParam());
std::vector<float> pcm(512 * 2, 0.25f);
SetPcm(port, pcm.data());
auto check_output = [&](const std::array<float, 2>& gain, float expected_pcm) {
CheckSamples(PushPcm(context, pcm.size() * sizeof(float)).at(0), {expected_pcm},
"gain modified stored PCM");
std::lock_guard lock(g_device_mutex);
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
"incorrect per-channel gain forwarded to backend");
};
check_output({1.0f, 1.0f}, 0.25f);
for (const std::array<float, 2> gain: {std::array {2.0f, 0.5f}, {0.0f, 0.0f}, {1.0f, 1.0f}}) {
SetAttribute(port, 1, gain);
check_output(gain, 0.25f);
check_output(gain, 0.25f);
}
const std::array gain {0.5f, 0.25f};
SetAttribute(port, 1, gain);
std::fill(pcm.begin(), pcm.end(), 0.75f);
SetPcm(port, pcm.data());
check_output(gain, 0.75f);
std::fill(pcm.begin(), pcm.end(), -0.25f);
const Pcm replacement {pcm.data()};
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
const std::array invalid_gain {1.0f, invalid};
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(attributes), 2) ==
static_cast<int32_t>(0x80268001u), "invalid gain was accepted");
check_output(gain, 0.75f);
}
for (const Attribute attribute: {Attribute {1, 0, nullptr, sizeof(gain)},
Attribute {1, 0, &gain, sizeof(float)}}) {
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
}
const Attribute duplicates[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &gain, sizeof(gain)}, {1, 0, &gain, sizeof(gain)}};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(duplicates), 3) ==
static_cast<int32_t>(0x80268001u), "duplicate gain attributes were accepted");
check_output(gain, 0.75f);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestObjectGainAndAmbisonics() {
constexpr float half_power = 0.70710678f;
struct Coefficients { uint32_t channel; float left; float right; };
constexpr Coefficients cases[] {{UINT32_MAX, half_power, half_power}, {0, 1.0f, 1.0f},
{1, 0.0f, 0.0f}, {2, half_power, -half_power}, {3, 0.0f, 0.0f},
{64, half_power, half_power}, {65, half_power, -half_power},
{66, 0.0f, 0.0f}, {99, 0.0f, 0.0f}};
for (const auto format: {0x100u, 0x101u}) {
const auto context = CreateContext(1, 1);
auto param = MakeParam(format);
param.port_type = 0x100;
const auto object = CreatePort(context, param);
Check(LiveDeviceCount() == 1, "object-only context did not create one output device");
std::vector<float> floating(512, 0.5f);
std::vector<int16_t> integer(512, 16384);
const void* pcm = format == 0x100 ? static_cast<const void*>(floating.data()) : integer.data();
constexpr auto output_bytes = 512 * 2 * sizeof(float);
SetPcm(object, pcm);
std::fill(floating.begin(), floating.end(), 0.0f);
std::fill(integer.begin(), integer.end(), 0);
SetAttribute(object, 1, 0.0f);
SetAttribute(object, 8, 65u);
for (int push = 0; push < 2; push++) {
const auto output = PushPcm(context, output_bytes);
Check(output.size() == 1, "object-only push did not output exactly once");
CheckSamples(output[0], {0.5f * half_power, 0.5f * half_power},
"object PCM ownership, default gain, or attribute freeze failed");
}
std::fill(floating.begin(), floating.end(), 0.5f);
std::fill(integer.begin(), integer.end(), 16384);
for (const auto& test: cases) {
Check(AudioOut2::AudioOut2ContextAdvance(context) == OK, "context advance failed");
const Pcm input {pcm};
const float gain = 0.5f;
const Attribute attributes[] {{0, 0, &input, sizeof(input)}, {1, 0, &gain, sizeof(gain)},
{8, 0, &test.channel, sizeof(test.channel)}};
Check(AudioOut2::AudioOut2PortSetAttributes(object, AsAttribute(attributes), 3) == OK,
"object attributes could not be set atomically with PCM first");
for (int push = 0; push < 2; push++) {
CheckSamples(PushPcm(context, output_bytes).at(0), {0.25f * test.left, 0.25f * test.right},
"object gain or ambisonics decoded incorrectly or accumulated across pushes");
}
}
AudioOut2::AudioOut2ContextAdvance(context);
SetAttribute(object, 8, UINT32_MAX);
SetAttribute(object, 1, 0.0f);
SetPcm(object, pcm);
CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "zero gain did not mute object");
AudioOut2::AudioOut2ContextAdvance(context);
SetPcm(object, pcm);
CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "object gain did not persist");
AudioOut2::AudioOut2ContextDestroy(context);
Check(LiveDeviceCount() == 0, "object context leaked its output device");
}
}
void TestObjectBusRouting() {
const auto context = CreateContext(1, 2);
const auto other_context = CreateContext(1, 1);
AudioOut2::AudioOut2UserHandle other_user = 0;
Check(AudioOut2::AudioOut2UserCreate(2000, &other_user) == OK, "second user create failed");
const auto first = CreatePort(context, MakeParam());
const auto second = CreatePort(context, MakeParam());
std::vector<float> bed(512 * 2, 0.125f);
SetPcm(first, bed.data());
SetPcm(second, bed.data());
auto param = MakeParam(0x100);
param.port_type = 0x100;
std::vector<float> object_pcm(512, 0.25f);
SetPcm(CreatePort(context, param), object_pcm.data());
param.user_handle = other_user;
SetPcm(CreatePort(context, param), object_pcm.data());
std::fill(object_pcm.begin(), object_pcm.end(), 1.0f);
SetPcm(CreatePort(other_context, param), object_pcm.data());
constexpr auto bytes = 512 * 2 * sizeof(float);
for (int push = 0; push < 2; push++) {
const auto output = PushPcm(context, bytes);
Check(output.size() == 3, "object bus was missing or queued more than once");
CheckSamples(output[0], {0.125f}, "objects modified first bed");
CheckSamples(output[1], {0.125f}, "objects modified second bed");
CheckSamples(output[2], {0.35355339f}, "MAIN objects failed to mix across users or leaked contexts");
}
SetPcm(first, nullptr);
SetPcm(second, nullptr);
CheckSamples(PushPcm(context, bytes).at(0), {0.35355339f}, "object output depended on bed PCM");
AudioOut2::AudioOut2ContextDestroy(context);
Check(LiveDeviceCount() == 1, "context destroy closed another context's object device");
CheckSamples(PushPcm(other_context, bytes).at(0), {0.70710678f}, "other context lost its object PCM");
AudioOut2::AudioOut2ContextDestroy(other_context);
Check(AudioOut2::AudioOut2UserDestroy(other_user) == OK, "second user destroy failed");
Check(LiveDeviceCount() == 0, "object bus device leaked");
}
} // namespace
namespace Libs::Audio::AudioInternal {
@@ -467,10 +644,14 @@ bool AudioOutHasDevice(int handle) {
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
g_output_gains.clear();
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);
g_output_gains.push_back(params[i].gains != nullptr
? std::array {params[i].gains[0], params[i].gains[1]}
: std::array {1.0f, 1.0f});
}
}
return 0;
@@ -499,6 +680,9 @@ int main() {
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
TestPortGainAndValidation();
TestObjectGainAndAmbisonics();
TestObjectBusRouting();
Check(AudioOut2::AudioOut2UserDestroy(g_user_handle) == OK, "test user destroy failed");
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
+74 -3
View File
@@ -7,6 +7,8 @@
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <memory>
#include <vector>
@@ -22,6 +24,7 @@ struct Stream {
SDL_AudioSpec spec;
uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption.
uint64_t updated;
std::vector<uint8_t> pcm;
};
uint64_t now = 1000000;
@@ -31,6 +34,7 @@ std::vector<std::unique_ptr<Stream>> streams;
bool fail_open = false, fail_put = false, stalled = false;
bool pad_connected = false, pad_bluetooth = false;
uint64_t pad_queue_us = 0;
const float* queued_pad_gains = nullptr;
int clears = 0;
Stream& GetStream(SDL_AudioStream* stream) {
@@ -61,7 +65,7 @@ SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* s
return nullptr;
}
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now}));
streams.push_back(std::make_unique<Stream>(Stream {*spec, 0, now, {}}));
return reinterpret_cast<SDL_AudioStream*>(streams.back().get());
}
@@ -80,12 +84,14 @@ int GetAudioStreamQueued(SDL_AudioStream* stream) {
state.spec.channels);
}
bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) {
bool PutAudioStreamData(SDL_AudioStream* stream, const void* data, int bytes) {
now += processing;
if (fail_put) {
return false;
}
auto& state = GetStream(stream);
const auto* pcm = static_cast<const uint8_t*>(data);
state.pcm.assign(pcm, pcm + bytes);
Drain(state);
state.frames += static_cast<uint64_t>(bytes) * 1000000 /
(SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels);
@@ -148,7 +154,8 @@ void Close(Stream*) {}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && pad_bluetooth;
}
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float) {
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float, const float* gains) {
queued_pad_gains = gains;
return stream != nullptr ? pad_queue_us : 0;
}
} // namespace DualSenseHaptics
@@ -181,6 +188,7 @@ struct Fixture {
fail_open = fail_put = stalled = false;
pad_connected = pad_bluetooth = false;
pad_queue_us = 0;
queued_pad_gains = nullptr;
sleeps.clear();
}
@@ -386,6 +394,68 @@ void TestZeroOutputFrequency() {
"internal output open accepted zero frequency");
Check(f.Open().ToInt() == 1, "rejected output port occupied a handle");
}
template <typename T>
void CheckOutputSamples(std::initializer_list<T> expected, const char* message) {
const auto& pcm = streams.back()->pcm;
Check(pcm.size() == expected.size() * sizeof(T) &&
std::memcmp(pcm.data(), expected.begin(), pcm.size()) == 0, message);
}
void TestChannelGains() {
Fixture f;
const std::array pcm {0.25f, -0.5f, 0.75f, -1.0f};
const auto stereo = f.audio.AudioOutOpen(0, 2, 48000, Audio::Format::FloatStereo);
std::array gains {2.0f, 0.5f};
Audio::OutputParam output {stereo, pcm.data(), gains.data()};
for (int push = 0; push < 2; push++) {
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.5f, -0.25f, 1.5f, -0.5f}, "float channel gains were not applied independently");
}
const int volume[] {16384, 32768};
f.audio.AudioOutSetVolume(stereo, 3, volume);
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.25f, -0.25f, 0.75f, -0.5f}, "channel gain did not combine with port volume");
gains = {0.0f, 0.0f};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.0f, -0.0f, 0.0f, -0.0f}, "zero channel gains did not mute");
output.gains = nullptr;
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples({0.125f, -0.5f, 0.375f, -1.0f}, "channel scaling changed original float PCM");
const std::array<int16_t, 2> integer_pcm {20000, -20000};
const auto integer = f.audio.AudioOutOpen(0, 1, 48000, Audio::Format::Signed16bitStereo);
output = {integer, integer_pcm.data(), gains.data()};
for (const auto gain: {2.0f, std::numeric_limits<float>::max()}) {
gains = {gain, gain};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples<int16_t>({32767, -32768}, "integer gain overflowed instead of saturating");
}
gains = {0.5f, 0.25f};
f.audio.AudioOutOutputs(&output, 1, false);
CheckOutputSamples<int16_t>({10000, -5000}, "integer channel gains changed original PCM");
const std::array surround_pcm {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f};
for (const auto format: {Audio::Format::Float8Ch, Audio::Format::Float12Ch}) {
const auto surround = f.audio.AudioOutOpen(0, 1, 48000, format);
output = {surround, surround_pcm.data(), surround_pcm.data()};
f.audio.AudioOutOutputs(&output, 1, false);
if (format == Audio::Format::Float8Ch) {
CheckOutputSamples({1.0f, 4.0f, 9.0f, 16.0f, 49.0f, 64.0f, 25.0f, 36.0f},
"channel reordering used destination channel gains");
} else {
CheckOutputSamples({82.0f, 104.0f, 9.0f, 16.0f, 170.0f, 208.0f, 25.0f, 36.0f},
"height downmix lost per-channel gains");
}
}
pad_connected = true;
const auto speaker = f.Open(2, 4);
gains = {0.75f, 0.25f};
output = {speaker, pcm.data(), gains.data()};
f.audio.AudioOutOutputs(&output, 1, false);
Check(queued_pad_gains != nullptr && queued_pad_gains[0] == gains[0] && queued_pad_gains[1] == gains[1],
"connected controller did not receive channel gains");
}
} // namespace
int main() {
@@ -399,6 +469,7 @@ int main() {
TestControllerSpeakerPacing();
TestInvalidBatchSize();
TestZeroOutputFrequency();
TestChannelGains();
Check(streams.empty(), "output stream leaked");
std::puts("AudioOutTimingTests: all cases passed");
}
+31 -1
View File
@@ -857,6 +857,36 @@ void CheckAprPaths(const std::filesystem::path &root) {
ids[0] == 0xffffffffu && ids[1] == expected_id && sizes[0] == 0 && sizes[1] == 3,
"APR foreach reports a missing path and continues to the valid file");
// These paths share the old 31-bit FNV-1a ID (0x122d1544).
const char *collision_paths[] = {"perf-audit/test_00015ddf.bin",
"perf-audit/test_000389b8.bin"};
Check(std::filesystem::create_directory(root / "perf-audit"), "create APR collision directory");
for (size_t i = 0; i < 2; ++i) {
Check(fixture.Create(root / collision_paths[i]), "create APR collision fixture");
fixture.Write(i == 0 ? "APR" : "OTHER", i == 0 ? 3 : 5);
fixture.Close();
}
uint32_t collision_ids[2] = {};
Check(resolve("/app0/", collision_paths, 2, collision_ids, sizes, &error_index) == OK &&
collision_ids[0] != collision_ids[1] && collision_ids[0] != 0xffffffffu &&
collision_ids[1] != 0xffffffffu && sizes[0] == 3 && sizes[1] == 5,
"APR assigns distinct valid IDs to colliding paths");
const auto *stat_symbol = symbols.FindByNid("ApkYaHb8Sek", Loader::SymbolType::Func);
const auto *size_symbol = symbols.FindByNid("WvEu7yl3Ivg", Loader::SymbolType::Func);
Check(stat_symbol && size_symbol, "APR file metadata exports are registered");
using Stat = int (KYTY_SYSV_ABI *)(uint32_t, FileSystem::FileStat *);
using Size = int (KYTY_SYSV_ABI *)(uint32_t, uint64_t *);
for (size_t i = 0; i < 2; ++i) {
FileSystem::FileStat stat {};
uint64_t size = 0;
Check(resolve("/app0/", &collision_paths[i], 1, ids, nullptr, &error_index) == OK &&
ids[0] == collision_ids[i] &&
reinterpret_cast<Stat>(stat_symbol->vaddr)(ids[0], &stat) == OK &&
reinterpret_cast<Size>(size_symbol->vaddr)(ids[0], &size) == OK &&
stat.st_size == static_cast<int64_t>(sizes[i]) && size == sizes[i],
"APR re-resolution preserves each file's ID, host path and cached size");
}
// PATH_MAX includes NUL; all components remain below NAME_MAX (255).
std::string longest = "/app0/";
for (int i = 0; i < 3; ++i) {
@@ -880,7 +910,7 @@ void CheckAprPaths(const std::filesystem::path &root) {
Check(resolve(unterminated.data(), paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects an unterminated prefix");
CheckAmprOrdering(symbols, expected_id);
CheckAmprOrdering(symbols, collision_ids[0]);
FileSystem::Umount("/app0");
}
+90 -17
View File
@@ -9,6 +9,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <semaphore>
#include <string>
#include <thread>
@@ -210,6 +211,7 @@ struct ProtectionCall {
};
std::vector<ProtectionCall> g_protection_log;
std::mutex g_protection_log_mutex;
void ResetProtectionLog() {
g_protection_calls = 0;
@@ -225,8 +227,11 @@ bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
protection = PAGE_READWRITE;
}
DWORD old_protection = 0;
g_protection_calls++;
g_protection_log.push_back({vaddr, size, mode});
{
std::lock_guard lock(g_protection_log_mutex);
g_protection_calls++;
g_protection_log.push_back({vaddr, size, mode});
}
return VirtualProtect(reinterpret_cast<void *>(vaddr), size, protection,
&old_protection) != 0;
}
@@ -719,24 +724,91 @@ void TestGpuDownloadProtectionMirrors() {
}
void TestCrossRegionUpload() {
constexpr uint64_t region_size = 4ull * 1024ull * 1024ull;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto &page_manager = harness.page_manager;
const auto page_size = page_manager.GetPageSize();
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
auto *memory = AllocateFixedGuestRange(region_size * 2, 0x10000);
const auto address = reinterpret_cast<uint64_t>(memory);
const auto boundary = (address + region_size - 1) & ~(region_size - 1);
uint32_t ranges = 0;
tracker.ForEachUploadRange(
boundary - page_size, page_size * 2, false,
[&](uint64_t, uint64_t) noexcept { ranges++; }, []() noexcept {});
Check(ranges == 2 &&
!tracker.IsRegionCpuModified(boundary - page_size, page_size * 2) &&
!IsWritable(reinterpret_cast<void *>(boundary - page_size)) &&
!IsWritable(reinterpret_cast<void *>(boundary)),
"cross-region upload did not clear and protect both regions");
tracker.MarkRegionAsCpuModified(boundary - page_size, page_size * 2);
for (const bool is_written : {false, true}) {
TrackerHarness harness;
auto &tracker = harness.tracker;
uint32_t ranges = 0;
tracker.ForEachUploadRange(
boundary - page_size, page_size * 2, is_written,
[&](uint64_t start, uint64_t size) noexcept {
Check(start == boundary - page_size + ranges * page_size &&
size == page_size,
"cold upload did not visit both region boundaries exactly");
ranges++;
},
[&]() noexcept {
Check(ranges == 2 &&
Protection(reinterpret_cast<void *>(boundary - page_size)) ==
PAGE_READONLY &&
Protection(reinterpret_cast<void *>(boundary)) == PAGE_READONLY,
"cold upload completed before clearing and protecting both regions");
});
for (const auto page : {boundary - page_size, boundary}) {
Check(!tracker.IsRegionCpuModified(page, page_size) &&
tracker.IsRegionGpuModified(page, page_size) == is_written &&
Protection(reinterpret_cast<void *>(page)) ==
(is_written ? PAGE_NOACCESS : PAGE_READONLY),
"cross-region upload did not preserve final ownership and protection");
}
Check(IsWritable(reinterpret_cast<void *>(boundary - 2 * page_size)) &&
IsWritable(reinterpret_cast<void *>(boundary + page_size)),
"cross-region upload changed neighboring pages");
tracker.UnmarkRegionAsGpuModified(boundary - page_size, 2 * page_size);
tracker.UntrackMemory(address, region_size * 2);
}
Release(memory);
}
void TestConcurrentColdUploads() {
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = AllocateFixedGuestRange(region_size * 2, region_size);
const auto address = reinterpret_cast<uint64_t>(memory);
std::counting_semaphore<2> start{0};
std::counting_semaphore<2> upload_entered{0};
std::counting_semaphore<2> finish_upload{0};
std::vector<std::jthread> workers;
for (const auto page : {address, address + region_size}) {
workers.emplace_back([&, page] {
start.acquire();
uint32_t ranges = 0;
tracker.ForEachUploadRange(
page, page_size, true,
[&](uint64_t upload_address, uint64_t upload_size) noexcept {
Check(upload_address == page && upload_size == page_size,
"concurrent cold upload lost its dirty page");
ranges++;
},
[&]() noexcept {
Check(ranges == 1, "concurrent cold upload skipped its dirty page");
upload_entered.release();
finish_upload.acquire();
});
});
}
start.release(2);
const bool first_entered = upload_entered.try_acquire_for(std::chrono::seconds(5));
const bool second_entered = upload_entered.try_acquire_for(std::chrono::seconds(5));
finish_upload.release(2);
for (auto &worker : workers) {
worker.join();
}
Check(first_entered && second_entered,
"cold writable uploads serialized disjoint regions");
for (const auto page : {address, address + region_size}) {
Check(!tracker.IsRegionCpuModified(page, page_size) &&
tracker.IsRegionGpuModified(page, page_size) &&
Protection(reinterpret_cast<void *>(page)) == PAGE_NOACCESS,
"concurrent cold upload did not retain GPU ownership");
tracker.UnmarkRegionAsGpuModified(page, page_size);
}
tracker.UntrackMemory(address, region_size * 2);
Release(memory);
}
@@ -1137,6 +1209,7 @@ int main(int argc, char **argv) {
TestExactDirtyIntervalsSharingTrackerPage();
TestGpuDownloadProtectionMirrors();
TestCrossRegionUpload();
TestConcurrentColdUploads();
TestUploadDoesNotSerializeDisjointRegion();
TestDownloadDoesNotSerializeDisjointRegion();
TestGpuUnmarkUsesRegionMask();
+5
View File
@@ -414,11 +414,16 @@ void TestFormatsAndVolume() {
Check(opened_device == 10 && opened_spec.channels == 4 && opened_spec.format == SDL_AUDIO_F32,
"did not select quad DualSense PCM");
ExpectPcm({0, 0, 0.5f, -0.125f, 0, 0, 1.0f, 0});
const std::array gains {0.25f, 2.0f};
Haptics::Queue(port.get(), 1, stereo.data(), 2, 2, true, volume.data(), 1.0f, gains.data());
ExpectPcm({0, 0, 0.125f, -0.25f, 0, 0, 0.25f, 0});
const std::array<int16_t, 2> mono {16384, -32768};
Queue(port, mono.data(), 2, 1, false, volume.data() + 1);
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f});
Queue(port, mono.data(), 2, 1, false, volume.data() + 1, 0.5f);
ExpectPcm({0, 0, 0.125f, 0.125f, 0, 0, -0.25f, -0.25f});
Haptics::Queue(port.get(), 1, mono.data(), 2, 1, false, volume.data() + 1, 1.0f, gains.data());
ExpectPcm({0, 0, 0.0625f, 0.0625f, 0, 0, -0.125f, -0.125f});
std::array<float, 24> multichannel {};
multichannel.fill(0.9f);
multichannel[0] = 0.1f;
+38 -2
View File
@@ -1,12 +1,15 @@
#include "common/virtualMemory.h"
#include "graphics/host_gpu/pageManager.h"
#include <barrier>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
@@ -167,6 +170,7 @@ struct ProtectionCall {
uint64_t size;
};
std::vector<ProtectionCall> g_protection_ranges;
std::mutex g_protection_log_mutex;
bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
Common::VirtualMemory::Mode mode) {
@@ -177,8 +181,11 @@ bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
protection = PAGE_READWRITE;
}
DWORD old_protection = 0;
g_protection_calls++;
g_protection_ranges.push_back({vaddr, size});
{
std::lock_guard lock(g_protection_log_mutex);
g_protection_calls++;
g_protection_ranges.push_back({vaddr, size});
}
return VirtualProtect(reinterpret_cast<void *>(vaddr), size, protection,
&old_protection) != 0;
}
@@ -547,6 +554,34 @@ void TestReadWriteWatcherInteractions() {
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
void TestConcurrentSharedWatchers() {
constexpr size_t worker_count = 8;
PageManager manager;
auto *memory = Allocate(TRACKER_PAGE_SIZE);
const auto address = reinterpret_cast<uint64_t>(memory);
bool watched = false;
std::barrier phase(worker_count, [&]() noexcept {
watched = !watched;
Check(Protection(memory) == (watched ? PAGE_READONLY : PAGE_READWRITE),
"concurrent watchers lost protection or failed to restore writes");
});
std::vector<std::jthread> workers;
for (size_t i = 0; i < worker_count; i++) {
workers.emplace_back([&] {
for (size_t round = 0; round < 100; round++) {
manager.UpdatePageWatchers<true>(address, TRACKER_PAGE_SIZE);
phase.arrive_and_wait();
manager.UpdatePageWatchers<false>(address, TRACKER_PAGE_SIZE);
phase.arrive_and_wait();
}
});
}
for (auto &worker : workers) {
worker.join();
}
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, "VirtualFree failed");
}
[[noreturn]] void RunDeathCase(const char *name) {
PageManager manager;
const auto page_size = manager.GetPageSize();
@@ -669,6 +704,7 @@ int main(int argc, char **argv) {
TestRegionMaskWatcherRanges();
TestRegionEndpointBatching();
TestReadWriteWatcherInteractions();
TestConcurrentSharedWatchers();
TestFatalPaths();
std::puts("PageManagerTests: all cases passed");
return 0;
File diff suppressed because it is too large Load Diff
+255
View File
@@ -24,6 +24,7 @@
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
@@ -60,6 +61,12 @@
#endif
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
namespace Libs::LibKernel::Memory {
bool TestWindowsBackingViewModes();
}
#endif
namespace Libs::Fiber {
struct FiberObject;
struct FiberOptParam;
@@ -458,6 +465,24 @@ void TestSparseBackingReadPreservesResidency() {
std::all_of(bytes.begin() + commit_size * 2, bytes.end(),
[](uint8_t value) { return value == 0xa7; }),
"sparse backing read did not copy resident pages and zero nonresident pages");
constexpr size_t prefix = 37, suffix = 53;
constexpr size_t clipped_size = prefix + commit_size + suffix;
std::fill(bytes.begin(), bytes.end(), 0x5a);
Check(test,
Libs::LibKernel::Memory::TryReadSparseBacking(
base + commit_size - prefix, bytes.data() + 1, clipped_size),
"sparse backing read rejected unaligned resident edges around a hole");
Check(test, bytes.front() == 0x5a && bytes[clipped_size + 1] == 0x5a,
"clipped sparse backing read overwrote destination canaries");
Check(test,
std::all_of(bytes.begin() + 1, bytes.begin() + 1 + prefix,
[](uint8_t value) { return value == 0x3c; }) &&
std::all_of(bytes.begin() + 1 + prefix, bytes.begin() + 1 + prefix + commit_size,
[](uint8_t value) { return value == 0; }) &&
std::all_of(bytes.begin() + 1 + prefix + commit_size,
bytes.begin() + 1 + clipped_size,
[](uint8_t value) { return value == 0xa7; }),
"clipped sparse backing read misplaced resident bytes or the intervening hole");
std::fill(bytes.begin(), bytes.end(), 0x5a);
Check(test, Libs::LibKernel::Memory::TryReadSparseBacking(base + commit_size, bytes.data(),
bytes.size()),
@@ -580,6 +605,107 @@ void TestSparseReadDuringDirectCommit() {
std::printf("[host] %-48s ok\n", test);
}
std::counting_semaphore<2>* g_backing_reads_entered = nullptr;
std::counting_semaphore<2>* g_continue_backing_reads = nullptr;
void ParkBackingRead(uintptr_t, size_t) {
g_backing_reads_entered->release();
g_continue_backing_reads->acquire();
}
void TestConcurrentBackingReads() {
using namespace Libs::LibKernel::Memory;
const char* test = "ConcurrentBackingReads";
constexpr uint64_t original = 0x123456789abcdef0ull;
for (const auto& readers: {std::pair {&TryReadBacking, &TryReadBacking},
std::pair {&TryReadBacking, &TryReadSparseBacking},
std::pair {&TryReadSparseBacking, &TryReadSparseBacking}}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"concurrent_backing_reads");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
std::array<uint64_t, 2> values {};
std::array<bool, 2> results {};
std::thread first([&] { results[0] = readers.first(base, &values[0], sizeof(values[0])); });
std::thread second(
[&] { results[1] = readers.second(base, &values[1], sizeof(values[1])); });
const bool concurrent = entered.try_acquire_for(std::chrono::seconds(5)) &&
entered.try_acquire_for(std::chrono::seconds(5));
// Release both threads even if an exclusive read lock prevents the second
// entry.
resume.release(2);
first.join();
second.join();
TestSetBackingReadCallback(nullptr);
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
Check(test, concurrent, "backing reads could not hold their locks concurrently");
Check(test, results[0] && results[1] && values[0] == original && values[1] == original,
"concurrent backing reads returned different contents");
}
std::printf("[host] %-48s ok\n", test);
}
void TestBackingReadExcludesWritesAndUnmap() {
using namespace Libs::LibKernel::Memory;
const char* test = "BackingReadExcludesWritesAndUnmap";
constexpr uint64_t original = 0x123456789abcdef0ull;
constexpr uint64_t replacement = 0xfedcba9876543210ull;
for (const auto read: {TryReadBacking, TryReadSparseBacking}) {
for (const bool unmap: {false, true}) {
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"backing_read_exclusion");
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
uint64_t snapshot = 0;
bool read_ok = false;
std::thread reader([&] { read_ok = read(base, &snapshot, sizeof(snapshot)); });
const bool reader_parked = entered.try_acquire_for(std::chrono::seconds(5));
std::binary_semaphore started {0};
std::binary_semaphore finished {0};
bool write_ok = false;
int unmap_result = OK;
std::thread mutation([&] {
started.release();
if (unmap) {
unmap_result = KernelMunmap(base, SceKernelPageSize);
} else {
write_ok = TryWriteBacking(base, &replacement, sizeof(replacement));
}
finished.release();
});
started.acquire();
const bool excluded = !finished.try_acquire_for(std::chrono::milliseconds(100));
resume.release();
reader.join();
mutation.join();
TestSetBackingReadCallback(nullptr);
uint64_t after = 0;
const bool still_backed = TryReadBacking(base, &after, sizeof(after));
if (!unmap) {
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
}
Check(test, reader_parked && excluded,
"backing write or unmap completed while a backing read held its "
"lock");
Check(test, read_ok && snapshot == original,
"backing mutation changed an in-progress read");
Check(test,
unmap ? unmap_result == OK && !still_backed
: write_ok && still_backed && after == replacement,
"backing mutation failed after the reader released its lock");
}
}
std::printf("[host] %-48s ok\n", test);
}
void TestGuestAddressSpaceHasNoFixedFallback() {
const char* test = "GuestAddressSpaceHasNoFixedFallback";
const auto unowned_address = reinterpret_cast<void*>(0x10000);
@@ -1355,6 +1481,118 @@ void TestPartialUnmapPreservesHostPermissions() {
}
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
void TestWindowsBackingViewPermissions() {
const char* test = "WindowsBackingViewPermissions";
Check(test, Libs::LibKernel::Memory::TestWindowsBackingViewModes(),
"backing view permissions differ from the requested mode");
std::printf("[host] %-48s ok\n", test);
}
#endif
void TestDirectMappingNamesTypesAndValidation() {
using namespace Libs::LibKernel::Memory;
const char* test = "DirectMappingNamesTypesAndValidation";
constexpr auto page = SceKernelPageSize;
int64_t physical = -1;
CheckOk(test, KernelAllocateDirectMemory(0, KernelGetDirectMemorySize(), page * 2, page,
SceKernelMtypeC, &physical),
"KernelAllocateDirectMemory");
void* named = nullptr;
const std::string long_name(KERNEL_MAXIMUM_NAME_LENGTH, 'x');
Check(test,
KernelMapNamedDirectMemory(nullptr, 0, SceKernelProtCpuExec, 0, -1, 0, long_name.c_str()) ==
Libs::LibKernel::KERNEL_ERROR_ENAMETOOLONG,
"named-map validation no longer checks the name first");
Check(test, KernelMapNamedDirectMemory(&named, page, SceKernelProtCpuRw, 0, physical, page,
nullptr) == Libs::LibKernel::KERNEL_ERROR_EFAULT && !named,
"null-name failure changed the output");
CheckOk(test, KernelReserveVirtualRange(&named, page * 2, 0, page), "KernelReserveVirtualRange");
const auto base = reinterpret_cast<uint64_t>(named);
const auto name = long_name.substr(1);
CheckOk(test, KernelMapNamedDirectMemory(&named, page, SceKernelProtCpuRw, SceKernelMapFixed,
physical, page, name.c_str()),
"KernelMapNamedDirectMemory");
void* second = reinterpret_cast<void*>(base + page);
CheckOk(test, KernelMapNamedDirectMemory(&second, page, SceKernelProtCpuRw, SceKernelMapFixed,
physical + page, page, "second"),
"KernelMapNamedDirectMemory(adjacent)");
ExpectRange(test, Query(test, base), base, base + page, SceKernelProtCpuRw,
0, 1, 0, 1, name.c_str(), physical);
ExpectRange(test, Query(test, base + page), base + page, base + page * 2, SceKernelProtCpuRw,
0, 1, 0, 1, "second", physical + page);
void* typed = nullptr;
constexpr int type = 2;
CheckOk(test, KernelMapDirectMemory2(&typed, page, type, SceKernelProtCpuRw, 0, physical, page),
"KernelMapDirectMemory2");
Check(test, Query(test, reinterpret_cast<uint64_t>(typed)).memory_type == type &&
Query(test, base).memory_type == SceKernelMtypeC,
"typed alias changed another mapping's type");
*static_cast<volatile uint64_t*>(typed) = 0x12345678;
Check(test, *static_cast<volatile uint64_t*>(named) == 0x12345678,
"typed mapping stopped aliasing its backing");
void* unchanged_type = nullptr;
CheckOk(test, KernelMapDirectMemory2(&unchanged_type, page, -1, SceKernelProtCpuRw, 0,
physical, page),
"KernelMapDirectMemory2(unchanged type)");
Check(test, Query(test, reinterpret_cast<uint64_t>(unchanged_type)).memory_type == SceKernelMtypeC,
"type -1 did not preserve the allocation's memory type");
void* plain = nullptr;
CheckOk(test, KernelMapDirectMemory(&plain, page, SceKernelProtCpuRw, 0, physical, page),
"KernelMapDirectMemory");
const auto plain_info = Query(test, reinterpret_cast<uint64_t>(plain));
Check(test, plain_info.memory_type == SceKernelMtypeC && plain_info.name[0] == '\0',
"plain mapping inherited another alias's type or name");
CheckOk(test, KernelReleaseDirectMemory(physical, page * 2), "KernelReleaseDirectMemory");
ExpectUnmapped(test, base);
ExpectUnmapped(test, reinterpret_cast<uint64_t>(typed));
ExpectUnmapped(test, reinterpret_cast<uint64_t>(plain));
ExpectUnmapped(test, reinterpret_cast<uint64_t>(unchanged_type));
std::printf("[host] %-48s ok\n", test);
}
void TestBatchMappingOperationsAndPartialFailure() {
using namespace Libs::LibKernel::Memory;
const char* test = "BatchMappingOperationsAndPartialFailure";
constexpr auto page = SceKernelPageSize;
int64_t physical = -1;
CheckOk(test, KernelAllocateDirectMemory(0, KernelGetDirectMemorySize(), page, page,
SceKernelMtypeC, &physical),
"KernelAllocateDirectMemory");
void* reservation = nullptr;
CheckOk(test, KernelReserveVirtualRange(&reservation, page * 2, 0, page), "KernelReserveVirtualRange");
const auto base = reinterpret_cast<uint64_t>(reservation);
void* flexible = reinterpret_cast<void*>(base + page);
std::array<KernelBatchMapEntry, 4> entries {{
{reservation, static_cast<uint64_t>(physical), page, SceKernelProtCpuRw, 0, 0, 0},
{flexible, 0, page, SceKernelProtCpuRw, 0, 0, 3},
{reservation, 0, page, SceKernelProtCpuRead, 0, 0, 2},
{flexible, 0, page, SceKernelProtCpuRead, 2, 0, 4},
}};
int processed = -1;
CheckOk(test, KernelBatchMap(entries.data(), entries.size(), &processed), "KernelBatchMap");
Check(test, processed == entries.size(), "batch did not process every operation");
ExpectRange(test, Query(test, base), base, base + page, SceKernelProtCpuRead,
0, 1, 0, 1, "anon", physical);
ExpectRange(test, Query(test, base + page), base + page, base + page * 2, SceKernelProtCpuRead,
1, 0, 0, 1, "anon");
std::array<KernelBatchMapEntry, 3> partial {{
{reservation, 0, page, 0, 0, 0, 1},
{nullptr, static_cast<uint64_t>(physical), page, SceKernelProtCpuRw, 0, 0, 0},
{flexible, 0, page, 0, 0, 0, 1},
}};
Check(test, KernelBatchMap(partial.data(), partial.size(), &processed) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL && processed == 1,
"failed fixed mapping did not preserve the completed prefix count");
ExpectUnmapped(test, base);
Check(test, Query(test, base + page).is_flexible == 1, "batch processed an entry after failure");
CheckOk(test, KernelBatchMap2(&partial.back(), 1, &processed, 0), "KernelBatchMap2(unmap)");
Check(test, processed == 1, "batch unmap count is wrong");
ExpectUnmapped(test, base + page);
CheckOk(test, KernelReleaseDirectMemory(physical, page), "KernelReleaseDirectMemory");
std::printf("[host] %-48s ok\n", test);
}
void TestDirectMapValidationBeforeOwnerMutation() {
const char* test = "DirectMapValidationBeforeOwnerMutation";
int64_t invalid = -1;
@@ -4178,6 +4416,16 @@ void TestSmallFiberStacksAndMigration() {
int main(int argc, char** argv) {
InitSubsystems();
if (argc == 2 && std::strcmp(argv[1], "--backing-transfers-only") == 0) {
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestDirectMapQueryOffsetAndPartialMunmap);
RunTest(TestFlexibleMemoryUsesSharedBacking);
RunTest(TestFlexibleMemoryReuseIsZeroFilled);
return g_failed_tests == 0 ? 0 : 1;
}
#if defined(__linux__)
if (argc == 2 && std::strcmp(argv[1], "--fixed-direct-replacement-only") == 0) {
RunTest(TestFixedDirectReplacementPreservesAccess);
@@ -4226,6 +4474,8 @@ int main(int argc, char** argv) {
RunTest(TestGuestAddressSpaceOwnsReservationsBeforeBacking);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestGuestAddressSpaceHasNoFixedFallback);
RunTest(TestGuestFreeRangeSearchDoesNotUnderflow);
RunTest(TestFlexibleMemoryCapacityIsBootFixed);
@@ -4249,6 +4499,11 @@ int main(int argc, char** argv) {
#if defined(__linux__)
RunTest(TestPartialUnmapPreservesHostPermissions);
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
RunTest(TestWindowsBackingViewPermissions);
#endif
RunTest(TestDirectMappingNamesTypesAndValidation);
RunTest(TestBatchMappingOperationsAndPartialFailure);
RunTest(TestDirectMapValidationBeforeOwnerMutation);
RunTest(TestDirectReleaseRollbackRestoresOwnerMapping);
RunTest(TestDirectReleaseContracts);
+136 -37
View File
@@ -6539,6 +6539,7 @@ void TestPerspectiveCentroidInputs() {
ShaderMapUserData(regs.ps_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
sh.ps_input_ena = sh.ps_input_addr = inputs;
sh.m_cbShaderMask = 0xf;
const std::array<Prospero::ColorComponentMapping, 8> mappings{};
ShaderPixelInputInfo pixel{};
(void)PrepareProgram(regs, sh, mappings, pixel);
@@ -6570,6 +6571,47 @@ void TestPerspectiveCentroidInputs() {
}
}
void TestPerspectiveSampleInputs() {
for (const uint32_t inputs : {1u, 3u, 5u, 7u}) {
const std::vector<uint32_t> shader = {
EncodeExp0(0x00, 0xf),
EncodeExp1(0, 1, inputs == 1 ? 0 : 2, inputs == 1 ? 1 : 3),
EncodeSopp(0x01)};
HW::PixelShaderInfo regs{};
regs.ps_regs.data_addr = reinterpret_cast<uint64_t>(shader.data());
ShaderMappedData mapped{};
mapped.code_size_bytes = shader.size() * sizeof(uint32_t);
ShaderMapUserData(regs.ps_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
sh.ps_input_ena = sh.ps_input_addr = inputs;
sh.m_cbShaderMask = 0xf;
const std::array<Prospero::ColorComponentMapping, 8> mappings{};
ShaderPixelInputInfo pixel{};
(void)PrepareProgram(regs, sh, mappings, pixel);
Check(pixel.ps_perspective_sample_vgpr == 0 &&
pixel.ps_system_input_base == 2u * std::popcount(inputs),
"perspective sample pair did not occupy the first enabled VGPRs");
const auto key = MakeStageStaticKey(pixel);
pixel.ps_perspective_sample_vgpr = UINT32_MAX;
Check(key != MakeStageStaticKey(pixel), "sample input is missing from shader key");
pixel.ps_perspective_sample_vgpr = 0;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.input_info.pixel = &pixel;
const auto result = RecompileForTest(shader, options);
const auto source = DisassembleSpirvBinary(result.spirv);
Check(SpirvContainsCapability(result.spirv, 52u) &&
SpirvHasDecorationValue(result.spirv, 11u, 18u) &&
source.find("InterpolateAtSample %gl_BaryCoordKHR") != std::string::npos &&
SpirvSourceHasInstructionUsing(source, "OpLoad", "%int %gl_SampleID") &&
SpirvSourceHasInstructionUsing(source, "OpCompositeExtract", " 1") &&
SpirvSourceHasInstructionUsing(source, "OpCompositeExtract", " 2"),
"sample I/J did not evaluate BaryCoordKHR Y/Z at SampleId");
Check(!SpirvHasDecorationValueWithDecoration(result.spirv, 11u, 5286u, 17u),
"sample interpolation changed the shared barycentric input location");
CheckSpirvBinaryValidates(result.spirv);
}
}
void TestPsInputCountRegisterDecode() {
HW::Context context;
// NUM_INTERP is 3 while bit 14 is an independent control flag that must be
@@ -10573,6 +10615,32 @@ void TestMergedShaderUserDataSnapshot() {
regs.gs_user_sgpr.value[HW::UserSgprInfo::SGPRS_MAX - 1]++;
Check(full.user_data.back() == 0x10001000u + HW::UserSgprInfo::SGPRS_MAX - 1,
"merged shader parameters borrowed mutable register storage");
regs.gs_regs.rsrc2.lds_size = 64;
regs.gs_regs.rsrc1.gs_vgpr_component_count = 1;
context.SetGsMaxVertOut(0);
for (const uint32_t wave_size : {32u, 64u}) {
context.SetShaderStages(wave_size == 32 ? 0x00400000u : 0u);
for (const uint16_t capacity : {64u, 128u, 256u}) {
context.SetMaxOutputPerSubgroup(capacity);
user_config.SetGeControl({capacity, capacity});
ShaderVertexInputInfo input{};
const auto params = PrepareProgram(regs, context, user_config, input);
Check(input.logical_stage == ShaderType::Mesh && input.mesh.wave_size == wave_size &&
input.mesh.lds_size_dwords == 8192 && input.mesh.max_vertices == capacity &&
input.mesh.max_primitives == capacity / 3 &&
input.mesh.primitives_per_group == capacity / 3 &&
input.mesh.vertices_per_group == capacity / 3 * 3 &&
input.mesh.threads_num[0] == capacity && params.back_code.empty() &&
params.user_data_count == 8u + HW::UserSgprInfo::SGPRS_MAX &&
params.user_data[0] == 0 && params.user_data[1] == 0 &&
std::equal(std::begin(regs.gs_user_sgpr.value), std::end(regs.gs_user_sgpr.value),
params.user_data.begin() + 8),
"non-amplifying NGG shader lost its workgroup, GE bounds, or native user SGPRs");
}
}
regs.gs_regs.rsrc2.lds_size = 0;
regs.gs_regs.rsrc1.gs_vgpr_component_count = 3;
context.SetShaderStages(0x20);
context.SetMaxOutputPerSubgroup(256);
context.SetGsMaxVertOut(8);
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriFan);
@@ -10830,7 +10898,7 @@ void TestMeshInputAssembly() {
struct Case {
Prospero::PrimitiveType topology;
uint32_t capacity, count, group, lane, width, address_low, base_vertex;
uint32_t wave_info, first, second, third, byte_offset, vertex_id;
uint32_t wave_info, group_info, first, second, third, byte_offset, vertex_id;
bool fetch;
uint32_t wave_size = 64;
bool fast_launch = false;
@@ -10838,51 +10906,63 @@ void TestMeshInputAssembly() {
};
std::vector<Case> cases = {
{Prospero::PrimitiveType::kTriList, 14, 177, 14, 2, 2, 0x1002, UINT32_MAX,
0x40000309, 6, 7, 8, 340, 0xabcc, true},
0x40000309, 0x00c09000, 6, 7, 8, 340, 0xabcc, true},
{Prospero::PrimitiveType::kTriList, 14, 177, 14, 9, 2, 0x1002, 0,
0x40000309, 27, 28, 29, 356, 0, false},
0x40000309, 0x00c09000, 27, 28, 29, 356, 0, false},
{Prospero::PrimitiveType::kTriList, 8, 180, 0, 64, 2, 0x1002, 0,
0x41000000, 192, 193, 194, 128, 0, false},
0x41000000, 0x00806000, 192, 193, 194, 128, 0, false},
{Prospero::PrimitiveType::kTriList, 8, 180, 1, 1, 1, 0x1000, 5,
0x40000206, 3, 4, 5, 4, 0xb0, true},
0x40000206, 0x00806000, 3, 4, 5, 4, 0xb0, true},
{Prospero::PrimitiveType::kTriList, 8, 180, 1, 1, 4, 0x1000, 5,
0x40000206, 3, 4, 5, 28, 0xabcd0128, true},
0x40000206, 0x00806000, 3, 4, 5, 28, 0xabcd0128, true},
{Prospero::PrimitiveType::kTriStrip, 5, 8, 1, 1, 0, 0, 11,
0x40000305, 1, 2, 3, 0, 15, false},
0x40000305, 0x00c05000, 1, 2, 3, 0, 15, false},
{Prospero::PrimitiveType::kTriStrip, 5, 8, 0, 1, 0, 0, 11,
0x40000305, 2, 1, 3, 0, 12, false},
0x40000305, 0x00c05000, 2, 1, 3, 0, 12, false},
{Prospero::PrimitiveType::kTriFan, 32, 6, 0, 3, 2, 0x1002, 0,
0x40000406, 0, 4, 5, 8, 0x0123, true},
0x40000406, 0x01006000, 0, 4, 5, 8, 0x0123, true},
{Prospero::PrimitiveType::kTriFan, 5, 8, 1, 0, 2, 0x1002, 0,
0x40000305, 0, 1, 2, 0, 0xabcd, true},
0x40000305, 0x00c05000, 0, 1, 2, 0, 0xabcd, true},
{Prospero::PrimitiveType::kTriFan, 5, 8, 1, 1, 0, 0, 11,
0x40000305, 0, 2, 3, 0, 15, false},
0x40000305, 0x00c05000, 0, 2, 3, 0, 15, false},
{Prospero::PrimitiveType::kTriFan, 5, 7, 1, 3, 2, 0x1002, 11,
0x40000204, 0, 4, 5, 12, 0xabd8, true},
0x40000204, 0x00804000, 0, 4, 5, 12, 0xabd8, true},
{Prospero::PrimitiveType::kTriFan, 5, 7, 1, 4, 2, 0x1002, 0,
0x40000204, 0, 5, 6, 16, 0, false},
0x40000204, 0x00804000, 0, 5, 6, 16, 0, false},
{Prospero::PrimitiveType::kTriFan, 5, 8, 1, 64, 4, 0x1000, 0,
0x41000000, 0, 65, 66, 268, 0, false},
0x41000000, 0x00c05000, 0, 65, 66, 268, 0, false},
{Prospero::PrimitiveType::kLineList, 5, 17, 3, 0, 0, 0, 11,
0x40000204, 0, 1, 0, 0, 23, false},
0x40000204, 0x00804000, 0, 1, 0, 0, 23, false},
{Prospero::PrimitiveType::kLineList, 6, 17, 2, 4, 2, 0x1002, 11,
0x40000205, 8, 9, 0, 32, 0xabd8, true},
0x40000205, 0x00805000, 8, 9, 0, 32, 0xabd8, true},
{Prospero::PrimitiveType::kLineList, 6, 17, 2, 5, 2, 0x1002, 0,
0x40000205, 10, 11, 0, 36, 0, false},
0x40000205, 0x00805000, 10, 11, 0, 36, 0, false},
{Prospero::PrimitiveType::kLineList, 8, 180, 1, 64, 4, 0x1000, 0,
0x41000000, 128, 129, 0, 288, 0, false},
0x41000000, 0x01008000, 128, 129, 0, 288, 0, false},
{Prospero::PrimitiveType::kPointList, 12, 265, 22, 0, 2, 0x1002, 0,
0x40000101, 0, 0, 0, 528, 0xabcd, true},
0x40000101, 0x00401000, 0, 0, 0, 528, 0xabcd, true},
{Prospero::PrimitiveType::kPointList, 12, 265, 22, 1, 2, 0x1002, 0,
0x40000101, 1, 0, 0, 532, 0, false},
0x40000101, 0x00401000, 1, 0, 0, 532, 0, false},
{Prospero::PrimitiveType::kPointList, 1, 1, 0, 0, 0, 0, 11,
0x40000101, 0, 0, 0, 0, 11, false},
0x40000101, 0x00401000, 0, 0, 0, 0, 11, false},
{Prospero::PrimitiveType::kPointList, 12, 265, 1, 1, 4, 0x1000, 5,
0x40000c0c, 1, 0, 0, 52, 0xabcd0128, true},
0x40000c0c, 0x0300c000, 1, 0, 0, 52, 0xabcd0128, true},
{Prospero::PrimitiveType::kPointList, 12, 265, 1, 64, 4, 0x1000, 0,
0x41000000, 64, 0, 0, 304, 0, false},
0x41000000, 0x0300c000, 64, 0, 0, 304, 0, false},
{Prospero::PrimitiveType::kTriStrip, 40, 40, 0, 32, 0, 0, 11,
0x81000608, 32, 33, 34, 0, 43, false, 32},
0x81000608, 0x09828000, 32, 33, 34, 0, 43, false, 32},
{Prospero::PrimitiveType::kTriList, 64, 64, 0, 0, 0, 0, 11,
0x1000153f, 0x0543f000, 0, 1, 2, 0, 11, false, 64, false, 64},
{Prospero::PrimitiveType::kTriList, 128, 128, 0, 64, 0, 0, 11,
0x2100003e, 0x0a87e000, 192, 193, 194, 0, 75, false, 64, false, 128},
{Prospero::PrimitiveType::kTriList, 256, 256, 0, 192, 0, 0, 11,
0x4300003f, 0x154ff000, 576, 577, 578, 0, 203, false, 64, false, 256},
{Prospero::PrimitiveType::kTriList, 64, 64, 0, 32, 0, 0, 11,
0x2100001f, 0x0543f000, 96, 97, 98, 0, 43, false, 32, false, 64},
{Prospero::PrimitiveType::kTriList, 128, 128, 0, 32, 0, 0, 11,
0x41000a20, 0x0a87e000, 96, 97, 98, 0, 43, false, 32, false, 128},
{Prospero::PrimitiveType::kTriList, 256, 256, 0, 64, 0, 0, 11,
0x82001520, 0x154ff000, 192, 193, 194, 0, 75, false, 32, false, 256},
};
for (const uint32_t wave_size : {32u, 64u}) {
for (const uint32_t threads : {wave_size, 2u * wave_size}) {
@@ -10893,7 +10973,7 @@ void TestMeshInputAssembly() {
(lane < wave_size ? 0x101u : 0u);
const uint32_t base_vertex = group == 11u ? UINT32_MAX : 11u;
cases.push_back({Prospero::PrimitiveType::kPointList, 1, 12, group, lane, 0, 0,
base_vertex, wave_info, 0, 0, 0, 0, group + base_vertex, false,
base_vertex, wave_info, 0x00401000, 0, 0, 0, 0, group + base_vertex, false,
wave_size, true, threads});
}
}
@@ -10956,15 +11036,16 @@ void TestMeshInputAssembly() {
ConstantPropagationPass(program.blocks);
}
std::array<uint32_t, 9> vgprs{};
uint32_t sgpr3 = 0;
std::array<uint32_t, 4> sgprs{};
for (const auto &inst : *program.blocks.front()) {
if (inst.GetOpcode() == ValueOpcode::SetVectorRegister) {
vgprs[RegIndex(inst.Arg(0).VectorRegister())] = inst.Arg(1).Resolve().U32();
} else if (inst.GetOpcode() == ValueOpcode::SetScalarRegister) {
sgpr3 = inst.Arg(1).Resolve().U32();
sgprs[RegIndex(inst.Arg(0).ScalarRegister())] = inst.Arg(1).Resolve().U32();
}
}
Check(sgpr3 == test.wave_info && vgprs[0] == ((test.first << 2) | (test.second << 18)) &&
Check(sgprs[2] == test.group_info && sgprs[3] == test.wave_info &&
vgprs[0] == ((test.first << 2) | (test.second << 18)) &&
vgprs[1] == test.third * 4 && vgprs[5] == test.vertex_id &&
vgprs[test.fast_launch ? 6 : 8] == 9u,
"mesh prolog changed input assembly, wave counts, vertex ID, or instance ID");
@@ -12981,9 +13062,23 @@ void TestRenderTargetReverseExportMapping() {
"inactive reverse MRT mapping was not normalized out of the shader "
"cache key");
sh.target_output_mode[0] = 4;
sh.m_cbShaderMask = 0xf;
PrepareProgram(regs, sh, mappings, compiled_info);
Check(compiled_info.target_export_mapping[0] == gr32.export_mapping,
"active reverse MRT mapping was lost before shader specialization");
const auto first_slot_key = MakeStageStaticKey(compiled_info);
sh.m_cbShaderMask = 0xf000;
mappings[3] = gr32.export_mapping;
PrepareProgram(regs, sh, mappings, compiled_info);
Check(compiled_info.target_shader_mask == sh.m_cbShaderMask &&
compiled_info.target_export_mapping[0] == gr32.export_mapping &&
first_slot_key != MakeStageStaticKey(compiled_info),
"compact MRT ordinal lost its physical export mapping or cache identity");
const auto sparse_slot_key = MakeStageStaticKey(compiled_info);
sh.m_cbShaderMask = 0x1000;
PrepareProgram(regs, sh, mappings, compiled_info);
Check(sparse_slot_key == MakeStageStaticKey(compiled_info),
"component-only shader mask changes introduced a duplicate shader variant");
}
void TestBlendMappingClassification() {
@@ -14675,13 +14770,14 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
EncodeSopp(0x01),
};
const auto structured_result = compile("structured-phi", structured_phi,
{.words = 140,
.instructions = 41,
{.words = 174,
.instructions = 49,
.phis = 1,
.labels = 8,
.loop_merges = 1,
.branches = 6,
.conditional_branches = 1});
.conditional_branches = 1,
.ballots = 2});
const auto structured_metrics = MeasureSpirv(structured_result.spirv);
Check((structured_result.ir_dump.find("Phi") != std::string::npos),
"structured Phi size fixture no longer contains an IR Phi");
@@ -14692,13 +14788,14 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
CheckSpirvPhiParents(structured_result.spirv);
const auto structured_repeat =
compile("structured-phi-repeat", structured_phi,
{.words = 140,
.instructions = 41,
{.words = 174,
.instructions = 49,
.phis = 1,
.labels = 8,
.loop_merges = 1,
.branches = 6,
.conditional_branches = 1});
.conditional_branches = 1,
.ballots = 2});
Check(structured_repeat.spirv == structured_result.spirv,
"deferred Phi patching is not deterministic");
@@ -14862,8 +14959,8 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
EncodeSopp(0x01),
};
const auto dispatcher_result = compile("dispatcher", dispatcher,
{.words = 242,
.instructions = 67,
{.words = 255,
.instructions = 70,
.variables = 3,
.function_variables = 3,
.loads = 3,
@@ -14874,7 +14971,8 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
.selection_merges = 1,
.branches = 10,
.conditional_branches = 1,
.switches = 1});
.switches = 1,
.ballots = 1});
Check(dispatcher_result.program.dispatcher_fallback &&
(dispatcher_result.ir_dump.find("Phi") != std::string::npos) &&
SpirvInstructionOpcodeCount(dispatcher_result.spirv, 245u) == 2u &&
@@ -15054,6 +15152,7 @@ int main() {
TestNewShaderRecompilerStageInputInfo();
TestCustomVintrpMovTranslation();
TestPerspectiveCentroidInputs();
TestPerspectiveSampleInputs();
TestGraphicsCreateInterpolantMapping();
TestNewShaderRecompilerPixelPipelineEntry();
TestComputeLdsAllocationIdentity();