Compare commits

..
Author SHA1 Message Date
nmzik 1be1c6c4f9 graphics: revert a358b847c command-processor display waits 2026-10-27 23:15:01 +01:00
M4andnmzik 69fdb03dc2 Launcher display options (#1938)
* launcher: add minimize and maximize buttons to the main window

The main window is a QDialog, which on Windows only gets a close button.

* graphics: keep the game's aspect ratio when presenting to the window

The guest frame was blitted across the whole swapchain image, so a window
or fullscreen display whose shape differs from the frame stretched the
picture. Fit the frame inside the window instead and clear the remaining
bars to black. The video-out overlay layer is drawn into the same
rectangle so it stays aligned with the frame underneath.

* launcher: accept custom screen resolutions such as 21:9

The resolution was a fixed enum of four 16:9 sizes. Make the combo box
editable so any WIDTHxHEIGHT between 320 and 16384 can be entered, and
add ultrawide presets (2560x1080, 3440x1440, 3840x1600, 5120x2160).

Settings saved by older launchers stored the enum key ("R1920X1080");
NormalizeResolution still accepts that form, so they load unchanged.

* launcher: add a Stretch to window option

Presentation now keeps the game's aspect ratio with black bars. Add
--stretch to fill the whole window as before, exposed in the launcher as
a "Stretch to window" checkbox under General, off by default.

* launcher: make the settings dialog resizable and its tabs scrollable

The dialog had no maximize button and pinned its minimum width to its
initial 900 px, and without scroll areas it could not be made shorter
than its content. Add a maximize button (not minimize, since the dialog
is modal), drop the width pin, and wrap each tab page in a frameless
scroll area.

With the scroll areas the layout's minimum size no longer forces the
dialog open, so size it explicitly: grow it to fit the largest tab,
capped at the available screen, when there is no saved geometry and
whenever the global settings add the game folders panel.

* launcher: simplify display options and preserve saved settings

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-27 21:59:56 +01:00
nmzik 2efd31443f audio: support 512 object ports per AudioOut2 context 2026-10-27 21:41:30 +01:00
nmzikandMajed-allababedi 9eef2e1434 graphics: reuse range-set nodes when merging additions
Co-authored-by: Majed-allababedi <60163390+Majed-allababedi@users.noreply.github.com>
2026-10-27 21:41:30 +01:00
nmzik 39faa62ebd controller: merge stick and sensor events into the newest unread pad sample 2026-10-27 21:15:03 +01:00
nmzik 7ff8aff5e6 audio: drain the AudioOut2 queue at an exact 48 kHz grain rate 2026-10-27 21:15:03 +01:00
nmzik 64a554dea7 audio: a pushed AudioOut2 grain consumes its ports' PCM 2026-10-27 21:15:03 +01:00
nmzik 4b0282e343 savedata: sort directory searches by the requested key 2026-10-27 21:15:03 +01:00
nmzik ffc81377b2 memory: refactor (#1941)
* memory: refactor

* tests: avoid Windows small macro collision

* memory: retain allocation nodes on partial release
2026-10-27 21:14:11 +01:00
bigjjmoneymanandnmzik 0685bffb2f texture cache: convert D16<->D24/D32 depth copies instead of crashing (#1873)
gpu: convert guest D16 depth between native D16 and stenciled four-byte host depth

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-27 10:41:22 +01:00
29 changed files with 2200 additions and 1628 deletions

No files matched your search

-2
View File
@@ -700,8 +700,6 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --stream-buffer-only)
add_test(NAME gpu_command_lane
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-command-lane-only)
add_test(NAME wait_until_safe_for_rendering
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --wait-until-safe-for-rendering-only)
add_test(NAME pm4_flip
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --flip-only)
add_test(NAME depth_bits
+4
View File
@@ -105,6 +105,10 @@ bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
bool StretchEnabled() {
return g_config->stretch_enabled;
}
bool VrEnabled() {
return g_config->vr_enabled;
}
+2
View File
@@ -64,6 +64,7 @@ struct ConfigOptions {
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool stretch_enabled = false;
bool splash_enabled = true;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
@@ -125,6 +126,7 @@ int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HdrEnabled();
bool HideCursorEnabled();
bool StretchEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
bool IsNetworkConnected();
+5 -16
View File
@@ -30,7 +30,6 @@
#include <mutex>
#include <semaphore>
#include <thread>
#include <utility>
#include <vector>
namespace Libs::Graphics {
@@ -92,12 +91,6 @@ void GuestGpu::SendCommand(Common::UniqueFunction<void>&& command) {
m_work_available.Signal();
}
void GuestGpu::Wake() {
Common::LockGuard lock(m_queue_mutex);
m_wake_pending = true;
m_work_available.Signal();
}
void GuestGpu::ProcessCommands() {
EXIT_IF(!IsGpuThread());
while (m_pending_commands.load(std::memory_order_acquire) != 0) {
@@ -443,10 +436,7 @@ void GuestGpu::ThreadRun(void* data) {
}
if (selected_queue < 0) {
gpu->m_processing = false;
// A wake that came before this wait must not be lost.
if (!std::exchange(gpu->m_wake_pending, false)) {
gpu->m_work_available.WaitFor(&gpu->m_queue_mutex, 100);
}
gpu->m_work_available.WaitFor(&gpu->m_queue_mutex, 100);
for (auto& queue: gpu->m_queues) {
if (!queue.empty()) {
queue.front().blocked = false;
@@ -960,11 +950,10 @@ void CommandProcessor::DrawIndexAuto(DrawAutoArgs args) {
}
void CommandProcessor::WaitFlipDone(uint32_t video_out_handle, uint32_t display_buffer_index) {
// Stall only this command processor, not the host command lane or other queues.
if (!m_renderer.GetVideoOut().IsSafeForRendering(static_cast<int>(video_out_handle),
static_cast<int>(display_buffer_index))) {
SuspendPm4();
}
BufferFlush();
m_renderer.GetVideoOut().WaitFlipDone(static_cast<int>(video_out_handle),
static_cast<int>(display_buffer_index));
}
void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector, void* address,
-3
View File
@@ -31,8 +31,6 @@ public:
[[nodiscard]] bool IsStopping();
void SendCommand(Common::UniqueFunction<void>&& command);
void SendCommandSync(Common::UniqueFunction<void>&& command);
// Retries blocked submissions now instead of after the 100 ms stall timeout.
void Wake();
// Submitted command memory is borrowed and must remain valid until GPU execution completes.
void Submit(std::span<const uint32_t> commands);
@@ -84,7 +82,6 @@ private:
bool m_processing = false;
bool m_accepting = true;
bool m_stopping = false;
bool m_wake_pending = false;
bool m_shutdown_complete = false;
// Completion callbacks can outlive GuestGpu during renderer shutdown.
std::shared_ptr<std::binary_semaphore> m_suspend_point_ready =
+13 -7
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <map>
namespace Libs::Graphics {
@@ -17,14 +18,19 @@ public:
if (it != m_ranges.begin() && std::prev(it)->second >= address) {
it = std::prev(it);
}
uint64_t begin = address;
uint64_t last = end;
while (it != m_ranges.end() && it->first <= last) {
begin = std::min(begin, it->first);
last = std::max(last, it->second);
it = m_ranges.erase(it);
if (it == m_ranges.end() || it->first > address) {
it = m_ranges.emplace_hint(it, address, end);
} else {
if (it->second >= end) {
return;
}
it->second = end;
}
auto next = std::next(it);
while (next != m_ranges.end() && next->first <= it->second) {
it->second = std::max(it->second, next->second);
next = m_ranges.erase(next);
}
m_ranges.emplace(begin, last);
}
void Subtract(uint64_t address, uint64_t size) {
+30 -30
View File
@@ -488,16 +488,15 @@ void TextureCache::RefreshCopySource(ImageId id) {
}
bool TextureCache::CopyD16(Image& destination, Image& source) {
const bool source_depth = source.info.IsDepth();
const bool destination_depth = destination.info.IsDepth();
if (source_depth == destination_depth) {
// Converts guest D16 between a four-byte host depth aspect and two-byte color or D16 depth.
const bool source_wide = DepthAspectTransferBytes(source.backing.format) == sizeof(uint32_t);
if (source_wide == (DepthAspectTransferBytes(destination.backing.format) == sizeof(uint32_t))) {
return false;
}
auto& depth = source_depth ? source : destination;
auto& color = source_depth ? destination : source;
const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format);
auto& depth = source_wide ? source : destination;
auto& narrow = source_wide ? destination : source;
if (depth.info.bytes_per_block != sizeof(uint16_t) ||
color.info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)) {
narrow.info.bytes_per_block != sizeof(uint16_t)) {
return false;
}
EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 ||
@@ -507,42 +506,42 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
const auto layers = depth.info.resources.layers;
const uint64_t depth_slice =
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * transfer_bytes;
const uint64_t color_slice =
static_cast<uint64_t>(color.info.pitch) * color.info.extent.height * sizeof(uint16_t);
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
const auto depth_size = depth_slice * layers;
const auto color_size = color_slice * layers;
const std::array depth_copies {
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * sizeof(uint32_t);
const uint64_t narrow_slice =
static_cast<uint64_t>(narrow.info.pitch) * narrow.info.extent.height * sizeof(uint16_t);
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || narrow_slice > UINT64_MAX / layers);
const std::array depth_copies {
vk::BufferImageCopy {0,
depth.info.pitch,
depth.info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
depth.info.extent}};
const std::array color_copies {
const std::array narrow_copies {
vk::BufferImageCopy {0,
color.info.pitch,
color.info.extent.height,
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
narrow.info.pitch,
narrow.info.extent.height,
{narrow.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor,
0, 0, layers},
{},
color.info.extent}};
narrow.info.extent}};
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
auto depth_buffer = m_tiler.GetScratchBuffer(depth_slice * layers);
auto narrow_buffer = m_tiler.GetScratchBuffer(narrow_slice * layers, depth_buffer.buffer);
const TileManager::D16Layout promote_layout {
.width = depth.info.extent.width,
.height = depth.info.extent.height,
.layers = layers,
.source_row_stride = static_cast<uint64_t>(color.info.pitch) * sizeof(uint16_t),
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * transfer_bytes,
.source_slice_stride = color_slice,
.source_row_stride = static_cast<uint64_t>(narrow.info.pitch) * sizeof(uint16_t),
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * sizeof(uint32_t),
.source_slice_stride = narrow_slice,
.target_slice_stride = depth_slice,
};
const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat;
if (source_depth) {
if (source_wide) {
source.Download(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size);
m_tiler.ConvertD16(depth_buffer, color_buffer, TileManager::D16Direction::Demote, d32,
m_tiler.ConvertD16(depth_buffer, narrow_buffer, TileManager::D16Direction::Demote, d32,
{.width = promote_layout.width,
.height = promote_layout.height,
.layers = promote_layout.layers,
@@ -550,11 +549,12 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
.target_row_stride = promote_layout.source_row_stride,
.source_slice_stride = promote_layout.target_slice_stride,
.target_slice_stride = promote_layout.source_slice_stride});
destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset,
color_buffer.size);
destination.Upload(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
narrow_buffer.size);
} else {
source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size);
m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
source.Download(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
narrow_buffer.size);
m_tiler.ConvertD16(narrow_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
promote_layout);
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
depth_buffer.size);
+16 -25
View File
@@ -221,7 +221,7 @@ public:
void WaitForSubmitSlot(VideoOutConfig& cfg);
bool Flip(uint32_t micros);
void GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out);
bool IsSafeForRendering(VideoOutConfig& cfg, int index);
void Wait(VideoOutConfig& cfg, int index);
private:
enum class RequestState { Reserved, Recording, Ready, Presenting };
@@ -240,7 +240,6 @@ private:
Graphics::Presenter::Frame* frame;
bool premultiplied_alpha;
};
void SignalDoneLocked();
Graphics::Presenter& m_presenter;
Common::Mutex m_mutex;
@@ -251,7 +250,6 @@ private:
std::list<Request> m_cpu_requests;
std::list<Request> m_cancelled_requests;
bool m_processing = false;
bool m_gpu_waiting = false;
uint64_t m_next_request_id = 1;
};
@@ -955,7 +953,7 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe
}
}
}
SignalDoneLocked();
m_done_cond_var.SignalAll();
}
if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
Common::DebugCounters::Add(Common::DebugCounters::Counter::FlipsRejected);
@@ -1035,7 +1033,7 @@ void FlipQueue::Cancel(VideoOutConfig& cfg) {
it = queue->erase(it);
}
}
SignalDoneLocked();
m_done_cond_var.SignalAll();
m_submit_slot_cond_var.SignalAll();
m_submit_cond_var.SignalAll();
m_mutex.Unlock();
@@ -1207,24 +1205,17 @@ void FlipQueue::WaitForSubmitSlot(VideoOutConfig& cfg) {
}
}
void FlipQueue::SignalDoneLocked() {
m_done_cond_var.SignalAll();
// A stalled GPU cannot drain and shut down without re-checking under m_mutex, so it is alive here.
if (m_gpu_waiting) {
m_gpu_waiting = false;
m_presenter.Renderer().GetGpu().Wake();
}
}
// A display buffer is safe for rendering once it is neither displayed nor waiting to be.
bool FlipQueue::IsSafeForRendering(VideoOutConfig& cfg, int index) {
void FlipQueue::Wait(VideoOutConfig& cfg, int index) {
Common::LockGuard lock(m_mutex);
auto matches = [&cfg, index](const auto& r) { return r.cfg == &cfg && r.index == index; };
const bool safe = cfg.flip_status.currentBuffer != index &&
std::none_of(m_requests.begin(), m_requests.end(), matches) &&
std::none_of(m_cpu_requests.begin(), m_cpu_requests.end(), matches);
m_gpu_waiting = !safe;
return safe;
auto has_request = [this, &cfg, index] {
auto matches = [&cfg, index](const auto& r) { return r.cfg == &cfg && r.index == index; };
return std::any_of(m_requests.begin(), m_requests.end(), matches) ||
std::any_of(m_cpu_requests.begin(), m_cpu_requests.end(), matches);
};
while (has_request()) {
m_done_cond_var.Wait(&m_mutex);
}
}
bool FlipQueue::Flip(uint32_t micros) {
@@ -1320,7 +1311,7 @@ bool FlipQueue::Flip(uint32_t micros) {
it = m_requests.erase(it);
}
m_processing = false;
SignalDoneLocked();
m_done_cond_var.SignalAll();
m_submit_slot_cond_var.SignalAll();
m_mutex.Unlock();
for (size_t i = count; i != 0; i--) {
@@ -1703,12 +1694,12 @@ void VideoOutDriver::WaitForSubmitSlot(int handle) {
m_impl->GetFlipQueue().WaitForSubmitSlot(*cfg);
}
bool VideoOutDriver::IsSafeForRendering(int handle, int index) {
void VideoOutDriver::WaitFlipDone(int handle, int index) {
auto* ctx = m_impl->Get(handle);
EXIT_IF(ctx == nullptr);
EXIT_NOT_IMPLEMENTED(!IsValidBufferIndex(index));
return m_impl->GetFlipQueue().IsSafeForRendering(*ctx, index);
m_impl->GetFlipQueue().Wait(*ctx, index);
}
KYTY_SYSV_ABI int VideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) {
+1 -1
View File
@@ -38,7 +38,7 @@ public:
void CompleteFlip(uint64_t request_id);
void SubmitFlipPreparation(uint64_t request_id);
void WaitForSubmitSlot(int handle);
[[nodiscard]] bool IsSafeForRendering(int handle, int index);
void WaitFlipDone(int handle, int index);
[[nodiscard]] Impl& State() noexcept;
+49 -14
View File
@@ -290,7 +290,8 @@ public:
private:
vk::SurfaceFormatKHR SelectFormat();
void Destroy();
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer,
const vk::Rect2D& rect);
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
@@ -637,7 +638,23 @@ bool Swapchain::PrepareSystemOverlay() {
return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount());
}
void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer) {
static vk::Rect2D FitAspect(const vk::Extent3D& source, const vk::Extent2D& target) {
const uint64_t src_w = std::max(source.width, 1u);
const uint64_t src_h = std::max(source.height, 1u);
uint32_t w = target.width;
uint32_t h = target.height;
if (src_w * h > src_h * w) {
h = std::max<uint32_t>(1, static_cast<uint32_t>(src_h * w / src_w));
} else {
w = std::max<uint32_t>(1, static_cast<uint32_t>(src_w * h / src_h));
}
return {{static_cast<int32_t>((target.width - w) / 2),
static_cast<int32_t>((target.height - h) / 2)},
{w, h}};
}
void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer,
const vk::Rect2D& rect) {
auto device = m_window.graphic_ctx.device;
if (m_overlay_pipeline == nullptr) {
vk::SamplerCreateInfo sampler {};
@@ -756,12 +773,14 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
command.beginRendering(&rendering);
const vk::Viewport viewport {
0.0f, 0.0f, static_cast<float>(m_extent.width), static_cast<float>(m_extent.height),
0.0f, 1.0f};
const vk::Rect2D scissor {{0, 0}, m_extent};
const vk::Viewport viewport {static_cast<float>(rect.offset.x),
static_cast<float>(rect.offset.y),
static_cast<float>(rect.extent.width),
static_cast<float>(rect.extent.height),
0.0f,
1.0f};
command.setViewport(0, 1, &viewport);
command.setScissor(0, 1, &scissor);
command.setScissor(0, 1, &rect);
command.draw(3, 1, 0, 0);
command.endRendering();
}
@@ -799,6 +818,24 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame*
vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags {}, 0,
nullptr, 0, nullptr, 1, &to_transfer);
const auto* aspect_source = source != nullptr ? source : overlay.frame;
const auto game_rect = !Config::StretchEnabled() && aspect_source != nullptr
? FitAspect(aspect_source->image.extent, m_extent)
: vk::Rect2D {{0, 0}, m_extent};
if (source == nullptr || game_rect.extent != m_extent) {
const vk::ClearColorValue black {};
vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal,
&black, 1, &to_transfer.subresourceRange);
if (source != nullptr) {
vk::ImageMemoryBarrier after_clear = to_transfer;
after_clear.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
after_clear.oldLayout = vk::ImageLayout::eTransferDstOptimal;
vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eTransfer,
vk::DependencyFlags {}, 0, nullptr, 0, nullptr, 1,
&after_clear);
}
}
if (source != nullptr) {
vk::ImageBlit region {};
region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
@@ -812,16 +849,14 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame*
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
region.dstOffsets[1].x = static_cast<int>(m_extent.width);
region.dstOffsets[1].y = static_cast<int>(m_extent.height);
region.dstOffsets[0].x = game_rect.offset.x;
region.dstOffsets[0].y = game_rect.offset.y;
region.dstOffsets[1].x = game_rect.offset.x + static_cast<int>(game_rect.extent.width);
region.dstOffsets[1].y = game_rect.offset.y + static_cast<int>(game_rect.extent.height);
region.dstOffsets[1].z = 1;
vk_command.blitImage(source->image.image, vk::ImageLayout::eTransferSrcOptimal,
m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1,
&region, vk::Filter::eLinear);
} else {
const vk::ClearColorValue black {};
vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal,
&black, 1, &to_transfer.subresourceRange);
}
vk::ImageMemoryBarrier to_present {};
@@ -848,7 +883,7 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame*
&to_present);
if (draw_attachment) {
if (draw_overlay) {
DrawOverlay(vk_command, overlay);
DrawOverlay(vk_command, overlay, game_rect);
if (draw_system_overlay) {
to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
+445 -1077
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -225,6 +225,11 @@ bool TestPlaceholderRangeIsFree(uint64_t vaddr, uint64_t size);
bool TestGuestAddressRangeIsOwned(uint64_t vaddr, uint64_t size);
bool TestGuestBackingOutsideAddressSpace();
uint64_t TestGuestBackingSize();
uint64_t TestFlexibleBackingHighWater();
uint64_t TestGuestFreeRangeVisits();
uint64_t TestDirectMappingCount();
uint64_t TestFlexibleMappingCount();
uint64_t TestBackingBytesZeroed();
bool TestGuestFreeRangeBounds();
#endif
+67 -59
View File
@@ -203,6 +203,9 @@ public:
}
std::lock_guard lock(m_mutex);
std::memset(m_backing_base + backing_offset, 0, size);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
g_test_backing_bytes_zeroed += size;
#endif
return true;
}
@@ -287,6 +290,18 @@ public:
return true;
}
bool ProtectViews(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
std::shared_lock lock(m_mutex);
bool ok = true;
ForEachSparseRange(
m_maps, vaddr, size,
[&](const auto&, uint64_t start, uint64_t bytes) {
ok = ok && VirtualMemory::Protect(start, bytes, mode);
},
[&](uint64_t, uint64_t) { ok = false; });
return ok;
}
bool MapFixed(uint64_t vaddr, uint64_t size, uint64_t backing_offset,
VirtualMemory::Mode mode, FailureReason* failure_reason = nullptr) {
if (!IsAvailable() || !RangeOk(backing_offset, size)) {
@@ -324,15 +339,11 @@ public:
#endif
}
bool Unmap(uint64_t vaddr, uint64_t size, bool* placeholder_preserved = nullptr) {
bool Unmap(uint64_t vaddr, uint64_t size) {
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
if (placeholder_preserved != nullptr) {
*placeholder_preserved = false;
}
const auto end = vaddr + size;
std::vector<MapEntry> targets;
{
@@ -350,42 +361,36 @@ public:
}
}
std::vector<MapEntry> removed;
removed.reserve(targets.size());
size_t removed = 0;
auto rollback = [&] {
while (removed != 0) {
const auto& previous = targets[--removed];
EXIT_IF(!MapFixed(previous.vaddr, previous.size, previous.backing_offset,
previous.mode));
}
};
for (const auto& entry: targets) {
bool part_placeholder_preserved = false;
if (!UnmapOne(entry.vaddr, entry.size, &part_placeholder_preserved)) {
for (auto it = removed.rbegin(); it != removed.rend(); ++it) {
EXIT_IF(!MapFixed(it->vaddr, it->size, it->backing_offset, it->mode));
}
if (placeholder_preserved != nullptr) {
*placeholder_preserved = false;
}
if (!UnmapOne(entry.vaddr, entry.size)) {
rollback();
return false;
}
EXIT_IF(!part_placeholder_preserved);
removed.push_back(entry);
++removed;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
// Each unmapped file view becomes a separate Windows placeholder. The address-space owner
// records this contiguous guest span as one free range, so normalize fragmented backing
// views into one placeholder before publishing it there.
if (removed.size() > 1 && !CoalescePlaceholders(vaddr, size)) {
for (auto it = removed.rbegin(); it != removed.rend(); ++it) {
EXIT_IF(!MapFixed(it->vaddr, it->size, it->backing_offset, it->mode));
}
if (removed > 1 && !CoalescePlaceholders(vaddr, size)) {
rollback();
return false;
}
#endif
if (placeholder_preserved != nullptr) {
*placeholder_preserved = true;
}
return true;
}
bool UnmapOne(uint64_t vaddr, uint64_t size, bool* placeholder_preserved = nullptr) {
bool UnmapOne(uint64_t vaddr, uint64_t size) {
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_backing_store_unmaps_before_failure == 0) {
g_test_backing_store_unmaps_before_failure = UINT32_MAX;
@@ -428,9 +433,7 @@ public:
m_maps.emplace(old.vaddr, old);
return false;
}
if (placeholder_preserved != nullptr) {
*placeholder_preserved = true;
}
return true;
}
@@ -451,9 +454,6 @@ public:
ok;
}
if (ok) {
if (placeholder_preserved != nullptr) {
*placeholder_preserved = true;
}
return true;
}
@@ -481,9 +481,6 @@ public:
std::lock_guard lock(m_mutex);
RemoveMappingsUnlocked(vaddr, size);
if (placeholder_preserved != nullptr) {
*placeholder_preserved = true;
}
return true;
#endif
}
@@ -634,8 +631,6 @@ private:
HANDLE m_handle = nullptr;
#elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX
[[nodiscard]] static uint64_t GetMappableTestSize() { return 0x4000; }
static int GetProtect(VirtualMemory::Mode mode) {
switch (mode) {
case VirtualMemory::Mode::Read: return PROT_READ;
@@ -760,7 +755,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 VirtualRangesOverlap(vaddr, size, range.first, range.second);
});
@@ -864,11 +859,7 @@ public:
if (!ContainsMappedUnlocked(vaddr, size, MappingType::Backing)) {
return false;
}
bool placeholder_preserved = false;
if (!m_backing->Unmap(vaddr, size, &placeholder_preserved)) {
return false;
}
if (!placeholder_preserved) {
if (!m_backing->Unmap(vaddr, size)) {
return false;
}
EXIT_IF(!RemoveMappedUnlocked(vaddr, size, MappingType::Backing));
@@ -876,12 +867,13 @@ public:
return true;
}
// Reservations live in VirtualRanges; these free-range checks only read the host placeholders.
uint64_t ReserveAligned(uint64_t search_addr, uint64_t size, uint64_t alignment) {
if (size == 0) {
return 0;
}
alignment = std::max<uint64_t>(alignment != 0 ? alignment : PageSize(), PageSize());
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
if (search_addr == 0) {
auto addr = FindFreeAlignedUnlocked(HOST_SYSTEM_MANAGED_MIN,
HOST_SYSTEM_MANAGED_MAX + 1u, size, alignment);
@@ -898,7 +890,7 @@ public:
return 0;
}
alignment = std::max<uint64_t>(alignment, PageSize());
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return FindFreeAlignedUnlocked(search_start, search_end, size, alignment);
}
@@ -906,19 +898,19 @@ public:
if (vaddr == 0 || size == 0 || !IsPageAligned(vaddr) || !IsPageAligned(size)) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return FindContaining(vaddr, size) != m_free.end();
}
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
bool TestContainsFree(uint64_t vaddr, uint64_t size) {
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return FindContaining(vaddr, size) != m_free.end();
}
#endif
bool ReleaseFree(uint64_t vaddr, uint64_t size) {
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return FindContaining(vaddr, size) != m_free.end();
}
@@ -1041,10 +1033,6 @@ private:
return (value & (PageSize() - 1u)) == 0;
}
[[nodiscard]] bool IsGranularityAligned(uint64_t value) const {
return (value & (m_granularity - 1u)) == 0;
}
bool ProtectMappedUnlocked(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
const auto end = vaddr + size;
auto it = m_mapped.upper_bound(vaddr);
@@ -1055,8 +1043,20 @@ private:
const auto& region = it->second;
const auto range_start = std::max(vaddr, region.vaddr);
const auto range_end = std::min(end, region.vaddr + region.size);
if (range_start < range_end &&
!VirtualMemory::Protect(range_start, range_end - range_start, mode)) {
if (range_start >= range_end) {
continue;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
// Rollback can split a backing view without changing the owner interval.
// Windows protection calls must stay within each actual view.
if (region.type == MappingType::Backing) {
if (!m_backing->ProtectViews(range_start, range_end - range_start, mode)) {
return false;
}
continue;
}
#endif
if (!VirtualMemory::Protect(range_start, range_end - range_start, mode)) {
return false;
}
}
@@ -1312,12 +1312,21 @@ private:
}
uint64_t FindFreeAlignedUnlocked(uint64_t search_start, uint64_t search_end, uint64_t size,
uint64_t alignment) {
uint64_t alignment) const {
if (search_start >= search_end || size > search_end - search_start) {
return 0;
}
for (const auto& [base, range_size]: m_free) {
const auto candidate = AlignUp(std::max(base, search_start), alignment);
// Include the predecessor because search_start can lie inside a free range.
auto it = m_free.upper_bound(search_start);
if (it != m_free.begin()) {
--it;
}
for (; it != m_free.end() && it->first < search_end; ++it) {
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
g_test_free_range_visits.fetch_add(1, std::memory_order_relaxed);
#endif
const auto& [base, range_size] = *it;
const auto candidate = AlignUp(std::max(base, search_start), alignment);
if (candidate < search_end && size <= search_end - candidate &&
GuestFreeRangeContains(base, range_size, candidate, size)) {
return candidate;
@@ -1331,9 +1340,8 @@ private:
if (next == m_free.begin()) {
return m_free.end();
}
auto it = std::prev(next);
auto end = it->first + it->second;
return (vaddr >= it->first && vaddr + size <= end ? it : m_free.end());
auto it = std::prev(next);
return GuestFreeRangeContains(it->first, it->second, vaddr, size) ? it : m_free.end();
}
bool SplitUnlocked(std::map<uint64_t, uint64_t>::iterator it, uint64_t vaddr, uint64_t size) {
@@ -115,7 +115,17 @@
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip"><string>Window resolution</string></property>
<property name="toolTip"><string>Window resolution as WIDTHxHEIGHT; pick a preset or type a custom size. Non-16:9 sizes such as 21:9 show the game with black bars unless Stretch to window is on.</string></property>
<property name="editable"><bool>true</bool></property>
<property name="insertPolicy"><enum>QComboBox::NoInsert</enum></property>
<item><property name="text"><string notr="true">1280x720</string></property></item>
<item><property name="text"><string notr="true">1920x1080</string></property></item>
<item><property name="text"><string notr="true">2560x1440</string></property></item>
<item><property name="text"><string notr="true">3840x2160</string></property></item>
<item><property name="text"><string notr="true">2560x1080</string></property></item>
<item><property name="text"><string notr="true">3440x1440</string></property></item>
<item><property name="text"><string notr="true">3840x1600</string></property></item>
<item><property name="text"><string notr="true">5120x2160</string></property></item>
</widget>
</item>
<item row="1" column="0">
@@ -256,6 +266,12 @@
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_stretch">
<property name="toolTip"><string>Stretch the game image to fill the window instead of keeping its aspect ratio with black bars</string></property>
<property name="text"><string>Stretch to window</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_perf_overlay">
<property name="toolTip"><string>Show FPS, frame times, CPU/GPU usage, RAM and memory pressure in the game window. Press F2 (Cmd+P on macOS) to toggle it, and add Shift for the detailed view.</string></property>
+10 -18
View File
@@ -21,10 +21,7 @@ inline QStringList EnumToList() {
auto me = QMetaEnum::fromType<T>();
int count = me.keyCount();
for (int i = 0; i < count; i++) {
auto key = QString(me.key(i));
ret << (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit()
? key.remove('R').toLower()
: key);
ret << QString(me.key(i));
}
return ret;
}
@@ -32,16 +29,12 @@ inline QStringList EnumToList() {
template <class T>
T TextToEnum(const QString& text) {
auto me = QMetaEnum::fromType<T>();
return static_cast<T>(me.keyToValue(
((text.size() > 1 && text.at(0).isDigit()) ? 'R' + text.toUpper() : text).toUtf8().data()));
return static_cast<T>(me.keyToValue(text.toUtf8().constData()));
}
template <class T>
QString EnumToText(T value) {
auto me = QMetaEnum::fromType<T>();
auto key = QString(me.valueToKey(static_cast<int>(value)));
return (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit() ? key.remove('R').toLower()
: key);
return QString(QMetaEnum::fromType<T>().valueToKey(static_cast<int>(value)));
}
struct ControllerSettings {
@@ -80,13 +73,10 @@ public:
static constexpr int DEFAULT_CONSOLE_LANGUAGE = 1;
static constexpr int MAX_CONSOLE_LANGUAGE = 29;
enum class Resolution {
R1280X720,
R1920X1080,
R2560X1440,
R3840X2160,
};
Q_ENUM(Resolution)
static constexpr int MIN_SCREEN_SIZE = 320;
static constexpr int MAX_SCREEN_SIZE = 16384;
static QString NormalizeResolution(const QString& text);
enum class ShaderOptimizationType { None, Size, Performance };
Q_ENUM(ShaderOptimizationType)
@@ -117,7 +107,7 @@ public:
// Controller preferences always come from the global configuration.
ControllerSettings controller;
Resolution screen_resolution = Resolution::R1280X720;
QString screen_resolution = QStringLiteral("1280x720");
QString user_name = "Kyty";
int user_id = Config::DEFAULT_USER_ID;
QString audio_input_device;
@@ -126,6 +116,7 @@ public:
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool stretch_enabled = false;
bool readback_linear_images = false;
bool readback_tiled_images = false;
bool sync_raw_image_buffers = false;
@@ -168,6 +159,7 @@ public:
fullscreen_enabled = other.fullscreen_enabled;
hdr_enabled = other.hdr_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
stretch_enabled = other.stretch_enabled;
readback_linear_images = other.readback_linear_images;
readback_tiled_images = other.readback_tiled_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
@@ -46,11 +46,15 @@ private:
QLineEdit* m_addons_dir_edit = nullptr;
QToolButton* m_remove_game_dir_button = nullptr;
bool m_global_settings = false;
bool m_geometry_restored = false;
protected:
void Init(const Configuration& info);
void InitGameDirectories();
void AddGameDirectoryItem(const QString& dir);
void MakeTabsScrollable();
void GrowToFitContents();
void ShowTab(QWidget* page);
void moveEvent(QMoveEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
+25 -2
View File
@@ -1,5 +1,23 @@
#include "configuration.h"
#include <QRegularExpression>
QString Configuration::NormalizeResolution(const QString& text) {
// Older settings stored the enum key, e.g. "R1920X1080".
static const QRegularExpression re(QStringLiteral("^\\s*R?(\\d+)\\s*[xX]\\s*(\\d+)\\s*$"));
const auto match = re.match(text);
if (!match.hasMatch()) {
return {};
}
const int width = match.captured(1).toInt();
const int height = match.captured(2).toInt();
if (width < MIN_SCREEN_SIZE || height < MIN_SCREEN_SIZE || width > MAX_SCREEN_SIZE ||
height > MAX_SCREEN_SIZE) {
return {};
}
return QStringLiteral("%1x%2").arg(width).arg(height);
}
QVariantMap Configuration::GameSettings() const {
QVariantMap values;
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
@@ -12,6 +30,7 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hdr_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(stretch_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(readback_tiled_images);
KYTY_CFG_SET(sync_raw_image_buffers);
@@ -48,7 +67,8 @@ QVariantMap Configuration::GameSettings() const {
template <class Settings>
void Configuration::ReadGameSettingsValues(const Settings& s) {
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
KYTY_CFG_GET(screen_resolution);
const auto resolution = NormalizeResolution(s.value("screen_resolution").toString());
screen_resolution = resolution.isEmpty() ? QStringLiteral("1280x720") : resolution;
user_name = s.value("user_name", user_name).toString();
bool user_id_ok = false;
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
@@ -64,6 +84,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hdr_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(stretch_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(readback_tiled_images);
KYTY_CFG_GET(sync_raw_image_buffers);
@@ -134,7 +155,9 @@ bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error)
if (!it.value().isString()) {
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
}
values.insert(it.key(), it.value().toString());
const auto value = it.value().toString();
values.insert(it.key(), it.key() == "screen_resolution" ? NormalizeResolution(value)
: value);
}
ReadGameSettingsValues(values);
+63 -9
View File
@@ -24,6 +24,8 @@
#include <QMessageBox>
#include <QPushButton>
#include <QResizeEvent>
#include <QScreen>
#include <QScrollArea>
#include <QSettings>
#include <QSlider>
#include <QSpinBox>
@@ -122,10 +124,10 @@ static QString GameDirectoryKey(const QString& dir) {
}
ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* parent)
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
m_info(info) {
: QDialog(parent, Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint),
m_ui(new Ui::ConfigurationEditDialog), m_info(info) {
m_ui->setupUi(this);
setMinimumWidth(width());
MakeTabsScrollable();
InitGameDirectories();
m_ui->controller_group->setVisible(false);
m_ui->settings_button->setVisible(false);
@@ -170,9 +172,11 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
setSizeGripEnabled(true);
restoreGeometry(g_last_geometry);
Init(info);
m_geometry_restored = restoreGeometry(g_last_geometry);
if (!m_geometry_restored) {
GrowToFitContents();
}
}
QByteArray ConfigurationEditDialog::g_last_geometry;
@@ -248,7 +252,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
info.audio_input_device);
}
microphone->setCurrentIndex(microphone->findData(info.audio_input_device));
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
m_ui->comboBox_screen_resolution->setCurrentText(info.screen_resolution);
ListInit(m_ui->comboBox_present_mode, info.present_mode);
m_ui->comboBox_gpu->clear();
m_ui->comboBox_gpu->addItem(tr("Auto"));
@@ -285,6 +289,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_stretch->setChecked(info.stretch_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_readback_tiled->setChecked(info.readback_tiled_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
@@ -413,8 +418,46 @@ void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs, const Q
m_game_dirs_group->setVisible(true);
m_ui->controller_group->setVisible(true);
update_game_directory_buttons();
if (!m_geometry_restored) {
GrowToFitContents();
}
}
void ConfigurationEditDialog::MakeTabsScrollable() {
auto* tabs = m_ui->settings_tabs;
for (int i = 0; i < tabs->count(); i++) {
auto* page = tabs->widget(i);
auto title = tabs->tabText(i);
auto* scroll = new QScrollArea;
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
tabs->removeTab(i);
scroll->setWidget(page);
tabs->insertTab(i, scroll, title);
}
tabs->setCurrentIndex(0);
}
void ConfigurationEditDialog::GrowToFitContents() {
layout()->activate();
resize(size().expandedTo(minimumSizeHint()));
auto* tabs = m_ui->settings_tabs;
QSize content;
for (int i = 0; i < tabs->count(); i++) {
content = content.expandedTo(
static_cast<QScrollArea*>(tabs->widget(i))->widget()->minimumSizeHint());
}
const QSize chrome = size() - tabs->currentWidget()->size();
resize(size().expandedTo(content + chrome).boundedTo(screen()->availableGeometry().size()));
}
void ConfigurationEditDialog::ShowTab(QWidget* page) {
auto* tabs = m_ui->settings_tabs;
for (int i = 0; i < tabs->count(); i++) {
if (tabs->widget(i)->isAncestorOf(page)) {
tabs->setCurrentIndex(i);
return;
}
}
}
QString ConfigurationEditDialog::GetAddonsDirectory() const {
@@ -474,13 +517,14 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
}
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
Configuration::NormalizeResolution(ui.comboBox_screen_resolution->currentText());
info.present_mode =
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hdr_enabled = ui.checkBox_hdr->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.stretch_enabled = ui.checkBox_stretch->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.readback_tiled_images = ui.checkBox_readback_tiled->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
@@ -518,7 +562,7 @@ void ConfigurationEditDialog::save() {
for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file,
m_ui->lineEdit_cmd_dump_folder}) {
if (field->isEnabled() && field->text().isEmpty()) {
m_ui->settings_tabs->setCurrentWidget(m_ui->debug_tab);
ShowTab(m_ui->debug_tab);
field->setFocus();
break;
}
@@ -540,6 +584,16 @@ void ConfigurationEditDialog::save() {
tr("User ID cannot be 254 (everyone) or 255 (system)"));
return;
}
if (Configuration::NormalizeResolution(m_ui->comboBox_screen_resolution->currentText())
.isEmpty()) {
ShowTab(m_ui->settings_tab);
m_ui->comboBox_screen_resolution->setFocus();
QMessageBox::critical(this, tr("Save failed"),
tr("Screen resolution must be WIDTHxHEIGHT, each between %1 and %2")
.arg(Configuration::MIN_SCREEN_SIZE)
.arg(Configuration::MAX_SCREEN_SIZE));
return;
}
UpdateInfo(m_info, *m_ui, m_global_settings);
+4 -2
View File
@@ -134,7 +134,8 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_ui->close_terminal->setVisible(false); // The emulator runs without a terminal.
#endif
main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/);
main_dialog->setWindowFlags(Qt::Dialog | Qt::WindowMinMaxButtonsHint |
Qt::WindowCloseButtonHint);
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
Qt::QueuedConnection);
@@ -240,7 +241,7 @@ static void AddFlag(QStringList& args, bool enabled, const char* name) {
static QStringList CreateEmulatorArgs(const Configuration& info) {
QStringList args;
auto r = EnumToText(info.screen_resolution).split('x');
auto r = Configuration::NormalizeResolution(info.screen_resolution).split('x');
if (r.size() != 2) {
return {};
@@ -266,6 +267,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
AddFlag(args, info.fullscreen_enabled, "--fullscreen");
AddFlag(args, info.hdr_enabled, "--hdr");
AddFlag(args, info.hide_cursor_enabled, "--hide-cursor");
AddFlag(args, info.stretch_enabled, "--stretch");
AddFlag(args, info.readback_linear_images, "--readback-linear-images");
AddFlag(args, info.readback_tiled_images, "--readback-tiled-images");
AddFlag(args, info.sync_raw_image_buffers, "--sync-raw-image-buffers");
+13
View File
@@ -519,7 +519,20 @@ void GameController::SetDevices(int active_id) {
m_trigger_state = {};
}
// Input that changes no button or touch contact updates the newest unread sample, so a burst of
// stick or sensor events between two reads cannot push unread button or touch changes out.
void GameController::AddState() {
if (m_states_num != 0) {
const auto newest = (m_first_state + m_states_num - 1) % STATES_MAX;
auto& last = m_states[newest];
if (!m_obtained[newest] && last.buttons == m_state.buttons &&
last.touch[0].down == m_state.touch[0].down &&
last.touch[1].down == m_state.touch[1].down) {
last = m_state;
return;
}
}
if (m_states_num >= STATES_MAX) {
m_states_num = STATES_MAX - 1;
m_first_state = (m_first_state + 1) % STATES_MAX;
+119 -88
View File
@@ -13,6 +13,7 @@
#include <atomic>
#include <cmath>
#include <cstring>
#include <span>
#include <vector>
namespace Libs::Audio {
@@ -168,17 +169,12 @@ struct AudioOut2MasteringStatesV2 {
static std::atomic_uint64_t g_audioout2_next_context {1};
static std::atomic_uint64_t g_audioout2_next_port {1};
static AudioOut2UserHandle g_audioout2_next_user = 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;
int object_audio_handle = 0;
};
static constexpr uint32_t AUDIOOUT2_CHANNEL_PORTS_MAX = 32;
static constexpr uint32_t AUDIOOUT2_OBJECT_PORTS_MAX = 512;
static constexpr uint32_t AUDIOOUT2_PORT_SLOT_BITS = 10;
static constexpr uint32_t AUDIOOUT2_CONTEXT_SLOT_BITS = 4;
struct AudioOut2PortStateEntry {
bool used = false;
@@ -196,6 +192,19 @@ struct AudioOut2PortStateEntry {
bool attributes_locked = false;
std::vector<uint8_t> pcm_data;
};
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;
int object_audio_handle = 0;
uint32_t max_ports = 0;
// Channel slots precede object slots; each partition has its own creation limit.
std::vector<AudioOut2PortStateEntry> ports;
};
struct AudioOut2SpeakerArrayState {
bool used = false;
uint32_t num_speakers = 0;
@@ -223,13 +232,16 @@ struct AudioOut2Submission {
static Common::Mutex g_audioout2_state_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static std::array<AudioOut2Submission, 16> g_audioout2_submissions;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
static std::array<AudioOut2SpeakerArrayState, 32> g_audioout2_speaker_arrays;
static Common::Mutex g_audioout2_latency_mutex;
static std::array<AudioOut2LatencyState, 16> g_audioout2_latencies;
static_assert(AUDIOOUT2_CHANNEL_PORTS_MAX + AUDIOOUT2_OBJECT_PORTS_MAX <=
(1u << AUDIOOUT2_PORT_SLOT_BITS));
static_assert(g_audioout2_contexts.size() == (1u << AUDIOOUT2_CONTEXT_SLOT_BITS));
static constexpr int AUDIO_OUT2_ERROR_NOT_READY = -2144960504; /* 0x80268008 */
static constexpr int AUDIO_OUT2_ERROR_PORT_FULL = -2144960494; /* 0x80268012 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511; /* 0x80268001 */
@@ -262,6 +274,11 @@ static AudioOut2Submission& audioout2_submission(const AudioOut2ContextState& co
return g_audioout2_submissions[&context - g_audioout2_contexts.data()];
}
// Ticks of a sixth of a microsecond: one 48 kHz sample lasts exactly 125 of them.
static uint64_t audioout2_clock() {
return LibKernel::KernelGetProcessTime() * 6;
}
static uint32_t audioout2_grain_micros(uint32_t grains) {
const auto sample_count = (grains == 0 ? 512u : grains);
return std::max<uint32_t>((sample_count * 1000000u) / 48000u, 1000u);
@@ -331,49 +348,51 @@ static void audioout2_update_context_locked(AudioOut2ContextState* state) {
return;
}
const auto now = LibKernel::KernelGetProcessTime();
const auto now = audioout2_clock();
if (state->last_update == 0 || state->queued == 0) {
state->last_update = now;
return;
}
const auto grain_micros = static_cast<uint64_t>(audioout2_grain_micros(state->num_grains));
if (now <= state->last_update || grain_micros == 0) {
if (now <= state->last_update) {
return;
}
const auto elapsed = now - state->last_update;
const auto drained = std::min<uint64_t>(state->queued, elapsed / grain_micros);
const auto grain = uint64_t {state->num_grains} * 125;
const auto drained = std::min<uint64_t>(state->queued, (now - state->last_update) / grain);
if (drained == 0) {
return;
}
state->queued -= static_cast<uint32_t>(drained);
state->last_update += drained * grain_micros;
state->last_update += drained * grain;
if (state->queued == 0) {
state->last_update = now;
}
}
static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle port) {
for (auto& state: g_audioout2_ports) {
if (state.used && state.handle == port) {
return &state;
}
const auto context_slot =
(port >> AUDIOOUT2_PORT_SLOT_BITS) & ((1u << AUDIOOUT2_CONTEXT_SLOT_BITS) - 1u);
const auto port_slot = port & ((1u << AUDIOOUT2_PORT_SLOT_BITS) - 1u);
auto& context = g_audioout2_contexts[context_slot];
if (context.used && port_slot < context.ports.size()) {
auto& state = context.ports[port_slot];
if (state.used && state.handle == port) return &state;
}
return nullptr;
}
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
const auto bytes_per_sample =
(state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
return static_cast<size_t>(state.samples_num) *
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) {
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == context.handle && !state.pcm_data.empty()) {
for (const auto& state: context.ports) {
if (state.used && !state.pcm_data.empty()) {
const int handle = audioout2_port_type_is_object(state.port_type)
? context.object_audio_handle
: state.audio_handle;
@@ -427,18 +446,21 @@ static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, AudioOut2S
out.object_mix.clear();
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
for (const auto& state: g_audioout2_ports) {
if (!state.used || state.context != ctx || state.pcm_data.empty()) {
// A port's PCM covers one grain: the push consumes it, so a port the game stops feeding
// falls silent.
for (auto& state: context->ports) {
if (!state.used || state.pcm_data.empty()) {
continue;
}
if (audioout2_port_type_is_object(state.port_type)) {
audioout2_mix_object(out.object_mix, context->num_grains, state);
} else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) {
const auto i = params.size();
out.pcm[i] = state.pcm_data;
out.pcm[i].swap(state.pcm_data);
out.gains[i] = state.gains;
params.push_back({state.audio_handle, out.pcm[i].data(), out.gains[i].data()});
}
state.pcm_data.clear();
}
if (!out.object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back({context->object_audio_handle, out.object_mix.data()});
@@ -467,22 +489,28 @@ int KYTY_SYSV_ABI AudioOut2ContextResetParam(AudioOut2ContextParam* params) {
EXIT_NOT_IMPLEMENTED(params == nullptr);
std::memset(params, 0, sizeof(AudioOut2ContextParam));
params->max_ports = 256;
params->max_object_ports = 256;
params->guarantee_object_ports = 0;
params->queue_depth = 4;
params->num_grains = 512;
params->flags = 1;
params->max_ports = 8;
params->queue_depth = 1;
params->num_grains = 256;
return OK;
}
static bool audioout2_valid_context_capacity(const AudioOut2ContextParam& params) {
return params.max_ports <= AUDIOOUT2_CHANNEL_PORTS_MAX &&
params.max_object_ports <= AUDIOOUT2_OBJECT_PORTS_MAX &&
params.guarantee_object_ports <= params.max_object_ports;
}
int KYTY_SYSV_ABI AudioOut2ContextQueryMemory(const AudioOut2ContextParam* params,
size_t* memory_size) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(memory_size == nullptr);
if (!audioout2_valid_context_capacity(*params)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth);
*memory_size = 0x10000u + static_cast<size_t>(queue_depth) * 0x590u;
@@ -498,6 +526,9 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(ctx == nullptr);
if (!audioout2_valid_context_capacity(*params)) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains;
const int object_audio_handle =
@@ -524,8 +555,10 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo
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->last_update = audioout2_clock();
state->object_audio_handle = object_audio_handle;
state->max_ports = params->max_ports;
state->ports.resize(params->max_ports + params->max_object_ports);
g_audioout2_state_mutex.Unlock();
LOGF("\t buffer = 0x%016" PRIx64 "\n"
@@ -541,8 +574,8 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx);
std::array<int, 257> audio_handles {};
size_t audio_handles_num = 0;
std::array<int, AUDIOOUT2_CHANNEL_PORTS_MAX + 1> audio_handles {};
size_t audio_handles_num = 0;
g_audioout2_state_mutex.Lock();
auto* state = audioout2_find_context_locked(ctx);
@@ -550,15 +583,12 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
if (state->object_audio_handle > 0) {
audio_handles[audio_handles_num++] = state->object_audio_handle;
}
*state = AudioOut2ContextState {};
}
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()) {
for (const auto& port_state: state->ports) {
if (port_state.used && port_state.audio_handle > 0) {
audio_handles[audio_handles_num++] = port_state.audio_handle;
}
port_state = AudioOut2PortStateEntry {};
}
*state = AudioOut2ContextState {};
}
g_audioout2_state_mutex.Unlock();
@@ -586,9 +616,7 @@ int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
g_audioout2_state_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state);
}
for (auto& port: g_audioout2_ports) {
if (port.used && port.context == ctx) {
for (auto& port: state->ports) {
port.attributes_locked = false;
}
}
@@ -612,7 +640,7 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth || use_device_clock) {
if (state->queued == 0) {
state->last_update = LibKernel::KernelGetProcessTime();
state->last_update = audioout2_clock();
}
if (state->queued < state->queue_depth) {
state->queued++;
@@ -663,21 +691,19 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
EXIT_NOT_IMPLEMENTED(params == nullptr);
EXIT_NOT_IMPLEMENTED(port == nullptr);
const auto next_port = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
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);
const bool is_object = audioout2_port_type_is_object(params->port_type);
g_audioout2_state_mutex.Lock();
const auto* context_state = audioout2_find_context_locked(ctx);
auto* context_state = audioout2_find_context_locked(ctx);
if (context_state == nullptr) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
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))) {
if (is_object && (params->port_type != 0x100 || 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;
}
@@ -688,42 +714,45 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
for (auto& candidate: g_audioout2_ports) {
if (!candidate.used) {
port_state = &candidate;
break;
}
}
if (port_state != nullptr) {
*port_state = AudioOut2PortStateEntry {};
port_state->used = true;
port_state->handle = next_port;
port_state->context = ctx;
port_state->user = params->user_handle;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
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_state_mutex.Unlock();
if (port_state == nullptr) {
auto slots = std::span(context_state->ports);
slots =
is_object ? slots.subspan(context_state->max_ports) : slots.first(context_state->max_ports);
const auto available = std::find_if(slots.begin(), slots.end(),
[](const auto& candidate) { return !candidate.used; });
if (available == slots.end()) {
g_audioout2_state_mutex.Unlock();
return AUDIO_OUT2_ERROR_PORT_FULL;
}
const auto serial = g_audioout2_next_port.fetch_add(1, std::memory_order_relaxed);
const auto context_slot = static_cast<uint64_t>(context_state - g_audioout2_contexts.data());
const auto port_slot = static_cast<uint64_t>(&*available - context_state->ports.data());
const auto next_port = (serial << (AUDIOOUT2_PORT_SLOT_BITS + AUDIOOUT2_CONTEXT_SLOT_BITS)) |
(context_slot << AUDIOOUT2_PORT_SLOT_BITS) | port_slot;
auto* port_state = &*available;
*port_state = AudioOut2PortStateEntry {};
port_state->used = true;
port_state->handle = next_port;
port_state->context = ctx;
port_state->user = params->user_handle;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
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_state_mutex.Unlock();
int audio_handle = 0;
if (audio_format != AudioInternal::Format::Unknown &&
!audioout2_port_type_is_object(params->port_type)) {
if (audio_format != AudioInternal::Format::Unknown && !is_object) {
audio_handle = AudioInternal::AudioOutOpen(audio_type, samples_num, params->sampling_freq,
audio_format);
}
g_audioout2_state_mutex.Lock();
const bool reserved = port_state->used && port_state->handle == next_port;
// Context destruction may have freed or replaced the slots while the backend opened.
port_state = audioout2_find_port_locked(next_port);
const bool reserved = port_state != nullptr;
if (reserved) {
port_state->audio_handle = audio_handle;
}
@@ -735,7 +764,7 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
*port = next_port;
if (next_port <= 16 || (next_port % 600) == 0) {
if (serial <= 16 || (serial % 600) == 0) {
PRINT_NAME();
LOGF("\t ctx = 0x%016" PRIx64 "\n"
"\t port = 0x%016" PRIx64 "\n"
@@ -901,21 +930,23 @@ 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_state_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
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;
}
if (std::any_of(g_audioout2_ports.begin(), g_audioout2_ports.end(),
[handle](const auto& port) { return port.used && port.user == handle; })) {
return AUDIO_OUT2_ERROR_BUSY;
for (const auto& context: g_audioout2_contexts) {
if (std::any_of(context.ports.begin(), context.ports.end(),
[handle](const auto& port) { return port.used && port.user == handle; })) {
return AUDIO_OUT2_ERROR_BUSY;
}
}
g_audioout2_users.erase(it);
return OK;
}
int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle,
uint32_t* context_attributes,
uint32_t* port_attributes) {
uint32_t* context_attributes,
uint32_t* port_attributes) {
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
+28 -10
View File
@@ -38,8 +38,8 @@ struct Global {
};
struct Shadow {
uintptr_t start = 0;
int count = 0;
uintptr_t start = 0;
uint64_t covered_bytes = 0;
};
struct AsanContext {
@@ -50,6 +50,14 @@ struct AsanContext {
static uint32_t g_fake_stack = 0;
static AsanContext* g_ctx = nullptr;
// Allocation and release callbacks can partition the same guest range differently.
static uint64_t ShadowCoverage(uintptr_t page, uintptr_t min_addr, uintptr_t max_addr) {
const auto guest_start = (page - 0x10000000000ull) << 3u;
const auto guest_end = guest_start + 0x80000ull - 1;
return std::min<uintptr_t>(max_addr, guest_end) - std::max<uintptr_t>(min_addr, guest_start) +
1;
}
static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
EXIT_IF(g_ctx == nullptr);
@@ -67,10 +75,11 @@ static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
static_cast<uint64_t>(min_shadow), static_cast<uint64_t>(size));
for (uintptr_t page = min_shadow; page <= max_shadow; page += page_size) {
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
[page](const Shadow& s) { return s.start == page; });
const auto bytes = ShadowCoverage(page, min_addr, max_addr);
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
[page](const Shadow& s) { return s.start == page; });
if (it != g_ctx->shadows.end()) {
it->count++;
it->covered_bytes += bytes;
} else {
if (!Common::VirtualMemory::AllocFixed(page, page_size,
Common::VirtualMemory::Mode::ReadWrite)) {
@@ -78,8 +87,8 @@ static void AllocShadow(uintptr_t min_addr, uintptr_t max_addr) {
}
Shadow s;
s.start = page;
s.count = 1;
s.start = page;
s.covered_bytes = bytes;
g_ctx->shadows.push_back(s);
}
}
@@ -105,14 +114,15 @@ static void FreeShadow(uintptr_t min_addr, uintptr_t max_addr) {
const auto it = std::find_if(g_ctx->shadows.begin(), g_ctx->shadows.end(),
[page](const Shadow& s) { return s.start == page; });
if (it != g_ctx->shadows.end()) {
it->count--;
if (it->count <= 0) {
const auto bytes = ShadowCoverage(page, min_addr, max_addr);
if (bytes >= it->covered_bytes) {
if (!Common::VirtualMemory::Free(page)) {
EXIT("can't free shadow memory\n");
}
g_ctx->shadows.erase(it);
} else {
it->covered_bytes -= bytes;
}
}
}
@@ -126,6 +136,14 @@ static void KernelFree(uintptr_t addr, size_t size) {
FreeShadow(addr, addr + size - 1);
}
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestRegisterMemoryCallbacks() {
EXIT_IF(g_ctx != nullptr);
g_ctx = new AsanContext;
LibKernel::Memory::RegisterCallbacks(KernelAlloc, KernelFree);
}
#endif
static void KYTY_SYSV_ABI asan_init() {
PRINT_NAME();
+31 -14
View File
@@ -615,7 +615,12 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
Common::LockGuard lock(g_mount_mutex);
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
const auto root = save_user_directory(title_id, cond->user_id);
std::vector<std::string> dir_list;
struct Found {
uint64_t key = 0;
std::string name;
auto operator<=>(const Found&) const = default;
};
std::vector<Found> dir_list;
if (Common::File::IsDirectoryExisting(root)) {
for (const auto& entry: Common::File::GetDirEntries(root)) {
@@ -625,22 +630,34 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
if (cond->dir_name == nullptr || cond->dir_name->data[0] == '\0' ||
dir_name_match(Common::ToLower(entry.name).c_str(),
Common::ToLower(std::string(cond->dir_name->data)).c_str())) {
dir_list.push_back(entry.name);
dir_list.push_back({.name = entry.name});
}
}
}
}
std::sort(dir_list.begin(), dir_list.end(), [](const std::string& a, const std::string& b) {
return std::strcmp(a.c_str(), b.c_str()) < 0;
});
if (cond->order == SaveDataSortOrder::Descent) {
std::vector<std::string> reversed;
for (size_t i = dir_list.size(); i > 0; i--) {
reversed.push_back(dir_list[i - 1]);
// A save whose metadata cannot be read sorts as 0.
for (auto& found: dir_list) {
const auto directory = root / found.name;
switch (cond->key) {
case SaveDataSortKey::UserParam: {
SaveDataParam param {};
read_save_param(save_param_path(directory), &param);
found.key = param.user_param;
break;
}
case SaveDataSortKey::Blocks:
case SaveDataSortKey::FreeBlocks: read_save_blocks(directory, &found.key); break;
case SaveDataSortKey::Mtime:
found.key = static_cast<uint64_t>(newest_save_time(directory));
break;
default: break;
}
dir_list = reversed;
}
// Directory names break ties; descending order reverses both.
std::sort(dir_list.begin(), dir_list.end());
if (cond->order == SaveDataSortOrder::Descent) {
std::reverse(dir_list.begin(), dir_list.end());
}
auto max_count =
@@ -651,9 +668,9 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
for (size_t i = 0; i < max_count; i++) {
std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s",
dir_list[i].c_str());
dir_list[i].name.c_str());
if (result->params != nullptr) {
const int status = load_save_param(root / dir_list[i], &result->params[i]);
const int status = load_save_param(root / dir_list[i].name, &result->params[i]);
if (status != OK) {
return status;
}
@@ -661,7 +678,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
if (result->infos != nullptr) {
auto& info = result->infos[i];
info = {};
const int status = read_save_blocks(root / dir_list[i], &info.blocks);
const int status = read_save_blocks(root / dir_list[i].name, &info.blocks);
if (status != OK) {
return status;
}
+3
View File
@@ -70,6 +70,8 @@ static void PrintUsage() {
::printf(" --hdr Enable HDR10 output when supported.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(
" --stretch Stretch the image to the window. Default: off.\n");
::printf(" --no-splash Disable the startup splash screen.\n");
::printf(" --vr Enable the virtual VR headset.\n");
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
@@ -199,6 +201,7 @@ static constexpr Flag FLAGS[] = {
{"--fullscreen", &ConfigOptions::fullscreen_enabled},
{"--hdr", &ConfigOptions::hdr_enabled},
{"--hide-cursor", &ConfigOptions::hide_cursor_enabled},
{"--stretch", &ConfigOptions::stretch_enabled},
{"--no-splash", &ConfigOptions::splash_enabled, false},
{"--vr", &ConfigOptions::vr_enabled},
{"--amd-cpu", &ConfigOptions::amd_cpu_enabled},
+225 -36
View File
@@ -35,6 +35,8 @@ int g_open_waiters = 0;
bool g_block_opens = false;
bool g_block_outputs = false;
std::vector<int> g_blocked_output;
std::atomic_uint64_t g_process_time {0};
std::atomic_bool g_process_time_frozen {false};
void Check(bool value, const char* text) {
if (!value) {
@@ -109,8 +111,10 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param;
}
AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) {
AudioOut2::AudioOut2ContextHandle
CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0, uint32_t max_ports = 8) {
ContextParam param {};
param.max_ports = max_ports;
param.max_object_ports = max_object_ports;
param.queue_depth = queue_depth;
param.num_grains = 512;
@@ -279,13 +283,139 @@ void TestSlotReuse() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestObjectPortCapacityPerContext() {
constexpr int port_full = static_cast<int32_t>(0x80268012u);
constexpr int invalid_port = static_cast<int32_t>(0x80268009u);
std::array<AudioOut2::AudioOut2ContextHandle, 3> contexts {};
std::array<AudioOut2::AudioOut2PortHandle, 3> last_ports {};
auto object = MakeParam(0x100);
object.port_type = 0x100;
for (size_t i = 0; i < contexts.size(); i++) {
contexts[i] = CreateContext(4, 512, 32);
CreatePort(contexts[i], MakeParam());
for (size_t j = 0; j < 512; j++) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(contexts[i], AsParam(&object), &port) == OK,
"context could not create all 512 object ports");
last_ports[i] = port;
}
AudioOut2::AudioOut2PortHandle overflow = UINT64_MAX;
Check(AudioOut2::AudioOut2PortCreate(contexts[i], AsParam(&object), &overflow) ==
port_full &&
overflow == UINT64_MAX,
"object capacity did not reject the 513th port without changing its output");
CreatePort(contexts[i], MakeParam());
}
for (size_t i = 0; i < contexts.size(); i++) {
const auto stale = last_ports[i];
AudioOut2::AudioOut2PortDestroy(stale);
last_ports[i] = CreatePort(contexts[i], object);
Check(last_ports[i] != stale &&
AudioOut2::AudioOut2PortSetAttributes(stale, nullptr, 0) == invalid_port,
"reused object slot accepted its old handle");
std::array<float, 512> pcm;
pcm.fill(0.25f * static_cast<float>(i + 1));
SetPcm(last_ports[i], pcm.data());
const auto output = PushPcm(contexts[i], 512 * 2 * sizeof(float));
const float expected = pcm[0] * 0.70710678f;
Check(output.size() == 1, "last object port did not reach its context's mixer");
CheckSamples(output[0], {expected, expected}, "object audio crossed context boundaries");
}
AudioOut2::AudioOut2ContextDestroy(contexts[0]);
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[0], nullptr, 0) == invalid_port,
"destroyed context retained its object handles");
const auto replacement = CreateContext(4, 512);
for (size_t i = 0; i < 512; i++) {
CreatePort(replacement, object);
}
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[0], nullptr, 0) == invalid_port,
"reused context accepted an old object handle");
for (size_t i = 1; i < contexts.size(); i++) {
Check(AudioOut2::AudioOut2PortSetAttributes(last_ports[i], nullptr, 0) == OK,
"destroying another context invalidated a live object port");
AudioOut2::AudioOut2ContextDestroy(contexts[i]);
}
AudioOut2::AudioOut2ContextDestroy(replacement);
Check(LiveDeviceCount() == 0, "object capacity test leaked audio devices");
}
void TestContextPortLimits() {
constexpr int port_full = static_cast<int32_t>(0x80268012u);
ContextParam param {};
param.max_ports = 1;
param.max_object_ports = 2;
param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
"small context create failed");
auto object = MakeParam(0x100);
object.port_type = 0x100;
const auto channel = CreatePort(context, MakeParam());
const auto first_object = CreatePort(context, object);
CreatePort(context, object);
AudioOut2::AudioOut2PortHandle overflow = 0;
const auto channel_param = MakeParam();
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) ==
port_full &&
AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
"declared channel/object capacities were ignored");
AudioOut2::AudioOut2PortDestroy(channel);
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
"object port borrowed a channel slot");
CreatePort(context, channel_param);
AudioOut2::AudioOut2PortDestroy(first_object);
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) == port_full,
"channel port borrowed an object slot");
CreatePort(context, object);
AudioOut2::AudioOut2ContextDestroy(context);
param.max_ports = param.max_object_ports = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
"empty context create failed");
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&channel_param), &overflow) ==
port_full &&
AudioOut2::AudioOut2PortCreate(context, AsParam(&object), &overflow) == port_full,
"zero port capacity was ignored");
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestContextCapacityParameters() {
constexpr int invalid_param = static_cast<int32_t>(0x80268001u);
ContextParam param;
std::memset(&param, 0xff, sizeof(param));
Check(AudioOut2::AudioOut2ContextResetParam(
reinterpret_cast<AudioOut2::AudioOut2ContextParam*>(&param)) == OK &&
param.max_ports == 8 && param.max_object_ports == 0 &&
param.guarantee_object_ports == 0 && param.num_grains == 256 &&
param.queue_depth == 1 && param.flags == 0 &&
std::all_of(std::begin(param.reserved), std::end(param.reserved),
[](uint32_t word) { return word == 0; }),
"context reset did not produce valid default parameters");
const auto defaults = param;
for (size_t i = 0; i < 3; i++) {
param = defaults;
if (i == 0) param.max_ports = 33;
if (i == 1) param.max_object_ports = 513;
if (i == 2) param.guarantee_object_ports = 1;
size_t memory = SIZE_MAX;
AudioOut2::AudioOut2ContextHandle context = UINT64_MAX;
Check(AudioOut2::AudioOut2ContextQueryMemory(AsParam(&param), &memory) == invalid_param &&
memory == SIZE_MAX &&
AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) ==
invalid_param &&
context == UINT64_MAX && LiveDeviceCount() == 0,
"invalid capacities allocated resources or modified outputs");
}
}
void TestFullTableRecovers() {
const auto context = CreateContext();
const auto param = MakeParam();
std::vector<AudioOut2::AudioOut2PortHandle> ports;
ports.reserve(256);
ports.reserve(8);
for (int i = 0; i < 256; i++) {
for (int i = 0; i < 8; i++) {
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"port table filled early");
@@ -322,6 +452,11 @@ void TestConcurrentCreates() {
});
}
WaitForDeviceOpens(thread_count);
AudioOut2::AudioOut2PortHandle overflow = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &overflow) ==
static_cast<int32_t>(0x80268012u) &&
overflow == 0,
"pending creates did not reserve their channel capacity");
ReleaseDeviceOpens();
for (auto& thread: threads) {
thread.join();
@@ -349,17 +484,31 @@ void TestContextDestroyCancelsPendingCreate() {
[&]() { result = AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port); });
WaitForDeviceOpens(1);
AudioOut2::AudioOut2ContextDestroy(context);
const auto replacement = CreateContext();
AudioOut2::AudioOut2PortHandle replacement_port = 0;
int replacement_result = OK;
std::thread replacement_creator([&]() {
replacement_result =
AudioOut2::AudioOut2PortCreate(replacement, AsParam(&param), &replacement_port);
});
WaitForDeviceOpens(2);
ReleaseDeviceOpens();
creator.join();
replacement_creator.join();
Check(result != OK, "destroyed context retained a pending port create");
Check(replacement_result == OK &&
AudioOut2::AudioOut2PortSetAttributes(replacement_port, nullptr, 0) == OK &&
LiveDeviceCount() == 1,
"cancelled create corrupted the replacement context or leaked a device");
AudioOut2::AudioOut2ContextDestroy(replacement);
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
}
void TestSynchronousDevicePushBypassesModelledQueue() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
@@ -368,6 +517,7 @@ void TestSynchronousDevicePushBypassesModelledQueue() {
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
SetPcm(port, pcm);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
"device-paced sync push was blocked by modelled queue");
const auto calls = OutputCalls();
@@ -447,6 +597,44 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPushConsumesPcm() {
const auto context = CreateContext();
const auto port = CreatePort(context, MakeParam());
uint32_t pcm[512] {};
SetPcm(port, pcm);
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push with PCM failed");
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push without new PCM failed");
SetPcm(port, pcm);
SetPcm(port, nullptr);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "push after clearing PCM failed");
Check(OutputCalls().size() == 1, "a push replayed PCM the game did not set again");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestQueueDrainsAtExactGrainRate() {
// Three 512-sample grains play in exactly 32000 us.
const auto context = CreateContext();
g_process_time_frozen = true;
const auto start = g_process_time.load();
for (int push = 0; push < 3; push++) {
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "async push failed");
}
const auto queued_after = [&](uint64_t micros) {
g_process_time = start + micros;
uint32_t queued = 0;
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, nullptr) == OK,
"queue-level query failed");
return queued;
};
Check(queued_after(31999) == 1, "the third grain drained before it finished playing");
Check(queued_after(32000) == 0, "three grains did not drain in their playing time");
g_process_time_frozen = false;
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPcmCopiedBeforeScratchBufferReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -486,7 +674,6 @@ 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");
@@ -494,10 +681,11 @@ void TestPortGainAndValidation() {
Check(g_output_gains.size() == 1 && g_output_gains[0] == gain,
"incorrect per-channel gain forwarded to backend");
};
SetPcm(port, pcm.data());
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);
SetPcm(port, pcm.data());
check_output(gain, 0.25f);
}
@@ -507,10 +695,11 @@ void TestPortGainAndValidation() {
SetPcm(port, pcm.data());
check_output(gain, 0.75f);
std::fill(pcm.begin(), pcm.end(), -0.25f);
const Pcm replacement {pcm.data()};
const std::vector<float> replacement_pcm(pcm.size(), -0.25f);
const Pcm replacement {replacement_pcm.data()};
for (const float invalid: {-1.0f, std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN()}) {
SetPcm(port, pcm.data());
const std::array invalid_gain {1.0f, invalid};
const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)},
{1, 0, &invalid_gain, sizeof(invalid_gain)}};
@@ -523,6 +712,7 @@ void TestPortGainAndValidation() {
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) ==
static_cast<int32_t>(0x80268001u), "malformed gain was accepted");
}
SetPcm(port, pcm.data());
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) ==
@@ -553,12 +743,10 @@ void TestObjectGainAndAmbisonics() {
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");
}
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) {
@@ -569,10 +757,8 @@ void TestObjectGainAndAmbisonics() {
{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");
}
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);
@@ -599,22 +785,21 @@ void TestObjectBusRouting() {
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());
const auto object = CreatePort(context, param);
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());
const auto other_user_object = CreatePort(context, param);
std::vector<float> object_pcm(512, 0.25f);
SetPcm(object, object_pcm.data());
SetPcm(other_user_object, object_pcm.data());
SetPcm(CreatePort(other_context, param), std::vector<float>(512, 1.0f).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);
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(object, object_pcm.data());
SetPcm(other_user_object, object_pcm.data());
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");
@@ -731,8 +916,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking)
namespace Libs::LibKernel {
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
static std::atomic_uint64_t now {0};
return now.fetch_add(1000);
return g_process_time_frozen ? g_process_time.load() : g_process_time.fetch_add(1000);
}
} // namespace Libs::LibKernel
@@ -741,6 +925,9 @@ int main() {
Check(AudioOut2::AudioOut2UserCreate(1000, &g_user_handle) == OK, "test user create failed");
TestUserSupportedAttributes();
TestSlotReuse();
TestObjectPortCapacityPerContext();
TestContextPortLimits();
TestContextCapacityParameters();
TestFullTableRecovers();
TestConcurrentCreates();
TestContextDestroyCancelsPendingCreate();
@@ -748,6 +935,8 @@ int main() {
TestFloat12ChannelPortOutputsPcm();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPushConsumesPcm();
TestQueueDrainsAtExactGrainRate();
TestPcmCopiedBeforeScratchBufferReuse();
TestPortGainAndValidation();
TestObjectGainAndAmbisonics();
+22
View File
@@ -689,6 +689,27 @@ void TestTriggerEffectState() {
check_state(0, 0, "switching controllers retained effect states");
}
void TestUnreadPressSurvivesStickBurst() {
Controller controller;
std::array<PadData, 64> data {};
SetButton(1, PAD_BUTTON_CROSS, true);
for (int value = 0; value < 100; value++) {
SetAxis(1, Axis::LeftX, value);
}
SetButton(1, PAD_BUTTON_CROSS, false);
for (int value = 100; value < 200; value++) {
SetAxis(1, Axis::LeftX, value);
}
Check(PadRead(1, data.data(), 64) == 2 && data[0].buttons == PAD_BUTTON_CROSS &&
data[0].left_stick_x == 99 && data[1].buttons == 0 && data[1].left_stick_x == 199,
"a stick burst pushed an unread press out of the pad history");
SetAxis(1, Axis::LeftX, 50);
SetButton(1, PAD_BUTTON_CROSS, true);
Check(PadRead(1, data.data(), 64) == 2 && data[0].buttons == 0 &&
data[0].left_stick_x == 50 && data[1].buttons == PAD_BUTTON_CROSS,
"a stick move after a read did not add a sample");
}
} // namespace
int main() {
@@ -702,6 +723,7 @@ int main() {
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
TestTriggerEffectState();
TestUnreadPressSurvivesStickBurst();
TestMultiplayerRouting();
TestDedicatedKeyboardOutputs();
Config::Shutdown();
+42
View File
@@ -660,6 +660,47 @@ void TestClassicSaveParams() {
CHECK(params[0].mtime == nested_mtime);
}
void TestSearchSortKeys() {
Reset("SORTED");
const std::array names {DirName("alpha"), DirName("bravo"), DirName("charlie")};
constexpr std::array<uint64_t, 3> blocks {96, 48, 48};
constexpr std::array<uint32_t, 3> user_params {2, 3, 1};
constexpr std::array<int, 3> hours_ahead {1, 3, 2};
for (size_t i = 0; i < names.size(); i++) {
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &names[i];
mount.mount_mode = 4;
mount.blocks = blocks[i];
SaveDataMountResult result {};
CHECK(SaveDataMount3(&mount, &result) == OK);
CHECK(SaveDataSetParam(&result.mount_point, 4, &user_params[i], sizeof(user_params[i])) ==
OK);
CHECK(SaveDataUmount2(0, &result.mount_point) == OK);
fs::last_write_time(fs::path("_SaveData/SORTED") / names[i].data / "sce_sys/param.bin",
fs::file_time_type::clock::now() + std::chrono::hours(hours_ahead[i]));
}
const auto search = [](SaveDataSortKey key, SaveDataSortOrder order) {
std::array<SceSaveDataDirName, 3> found {};
SaveDataDirNameSearchCond cond {};
cond.user_id = 1;
cond.key = key;
cond.order = order;
SaveDataDirNameSearchResult result {};
result.dir_names = found.data();
result.dir_names_num = found.size();
CHECK(SaveDataDirNameSearch(&cond, &result) == OK && result.set_num == 3);
return std::string(found[0].data) + " " + found[1].data + " " + found[2].data;
};
CHECK(search(SaveDataSortKey::DirName, SaveDataSortOrder::Descent) == "charlie bravo alpha");
CHECK(search(SaveDataSortKey::UserParam, SaveDataSortOrder::Ascent) == "charlie alpha bravo");
CHECK(search(SaveDataSortKey::Blocks, SaveDataSortOrder::Ascent) == "bravo charlie alpha");
CHECK(search(SaveDataSortKey::FreeBlocks, SaveDataSortOrder::Descent) == "alpha charlie bravo");
CHECK(search(SaveDataSortKey::Mtime, SaveDataSortOrder::Descent) == "bravo charlie alpha");
CHECK(fs::remove("_SaveData/SORTED/alpha/sce_sys/blocks.bin"));
CHECK(search(SaveDataSortKey::Blocks, SaveDataSortOrder::Ascent) == "alpha bravo charlie");
}
void RunChild(const fs::path& executable, const char* mode) {
#ifdef _WIN32
CHECK(_spawnl(_P_WAIT, executable.string().c_str(), executable.string().c_str(), mode,
@@ -706,6 +747,7 @@ int main(int argc, char** argv) {
TestGuestSaveIsolation();
TestSaveAllocations();
TestClassicSaveParams();
TestSearchSortKeys();
CHECK(SaveDataTerminate() == OK);
fs::current_path(previous);
fs::remove_all(temp);
+71 -124
View File
@@ -5686,13 +5686,13 @@ public:
"unmapping the table kept its images pinned");
// A smaller table enters once a second draw acquires it and leaves at the suspend point
// after a frame that did not acquire it. Larger tables stay.
const IR::ImageHeapRequest small{table, table_size / 2u, 32u, 0u, table_size / 64u, &sampled};
const IR::ImageHeapRequest small_request{table, table_size / 2u, 32u, 0u, table_size / 64u, &sampled};
Require(name, "first small draw",
acquire(sampled, idle_table) && !heap.Acquire(small, view) &&
!heap.Acquire(small, view),
acquire(sampled, idle_table) && !heap.Acquire(small_request, view) &&
!heap.Acquire(small_request, view),
"a smaller table entered on its first draw");
draw();
Require(name, "second small draw", heap.Acquire(small, view),
Require(name, "second small draw", heap.Acquire(small_request, view),
"a smaller table was declined on its second draw");
context.RunGarbageCollector();
Require(name, "acquired frame", ImageHeapTestAccess::MemoryCount(heap) == 2u,
@@ -9978,37 +9978,78 @@ public:
d16_storage_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage;
const auto d16_storage_image =
texture_cache.FindImage(d16_storage_desc, true);
auto d16_storage_readback = CreateHostBuffer(
name, sizeof(d16_fallback_values),
vk::BufferUsageFlagBits::eTransferDst, std::vector<u32>(2, 0));
auto &d16_storage_native = texture_cache.GetImage(d16_storage_image);
d16_storage_native.Transit(vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead, {},
scheduler.Current().Handle());
vk::BufferImageCopy d16_storage_copy{};
d16_storage_copy.bufferRowLength = 4;
d16_storage_copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0,
0, 1};
d16_storage_copy.imageExtent = {4, 1, 1};
scheduler.Current().Handle().copyImageToBuffer(
d16_storage_native.backing.image,
vk::ImageLayout::eTransferSrcOptimal, d16_storage_readback.buffer, 1,
&d16_storage_copy);
HostReadBarrier(d16_storage_readback.buffer, d16_storage_readback.size,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite);
scheduler.Finish();
const auto d16_storage_words = ReadBuffer(name, d16_storage_readback, 2);
std::array<uint16_t, 4> d16_storage_values{};
std::memcpy(d16_storage_values.data(), d16_storage_words.data(),
sizeof(d16_storage_values));
const auto read_texels = [&](ImageId id, vk::ImageAspectFlagBits aspect) {
auto readback = CreateHostBuffer(name, 4 * sizeof(u32),
vk::BufferUsageFlagBits::eTransferDst,
std::vector<u32>(4, 0));
auto &image = texture_cache.GetImage(id);
image.Transit(vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead, {},
scheduler.Current().Handle());
const vk::BufferImageCopy copy{0, 4, 0, {aspect, 0, 0, 1}, {}, {4, 1, 1}};
scheduler.Current().Handle().copyImageToBuffer(
image.backing.image, vk::ImageLayout::eTransferSrcOptimal,
readback.buffer, 1, &copy);
HostReadBarrier(readback.buffer, readback.size,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite);
scheduler.Finish();
auto words = ReadBuffer(name, readback, 4);
DestroyBuffer(&readback);
return words;
};
const auto d16_storage_words =
read_texels(d16_storage_image, vk::ImageAspectFlagBits::eColor);
Require(
name, "D16 fallback depth/storage copy",
d16_storage_values == d16_fallback_values &&
std::memcmp(d16_storage_words.data(), d16_fallback_values.data(),
sizeof(d16_fallback_values)) == 0 &&
d16_storage_image != d16_depth_image &&
texture_cache.GetImage(d16_storage_image).IsGpuModified(),
"D16 fallback reinterpreted four-byte host depth as two-byte color");
DestroyBuffer(&d16_storage_readback);
// Dropping and re-adding the stencil plane keeps the D16 depth values.
constexpr uint64_t d16_stenciled_offset = 0x33e000;
constexpr std::array<uint16_t, 4> d16_stale_values{};
std::memcpy(memory + d16_stenciled_offset, d16_fallback_values.data(),
sizeof(d16_fallback_values));
auto d16_stenciled_desc = d16_depth_desc;
d16_stenciled_desc.info.data.address = base + d16_stenciled_offset;
d16_stenciled_desc.info.stencil.address = base + 0x33f000;
const auto d16_stenciled_image =
texture_cache.FindImage(d16_stenciled_desc);
(void)texture_cache.FindDepthTarget(d16_stenciled_image,
d16_stenciled_desc);
texture_cache.MarkGpuWritten(d16_stenciled_image);
// Stale guest bytes: the values below can come only from image copies.
Libs::LibKernel::Memory::WriteBacking(base + d16_stenciled_offset,
d16_stale_values.data(),
sizeof(d16_stale_values));
auto d16_native_desc = d16_stenciled_desc;
d16_native_desc.info.stencil = {};
d16_native_desc.info.pixel_format = vk::Format::eD16Unorm;
d16_native_desc.view_info.format = vk::Format::eD16Unorm;
d16_native_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth;
const auto d16_native_image = texture_cache.FindImage(d16_native_desc);
const auto d16_native_words =
read_texels(d16_native_image, vk::ImageAspectFlagBits::eDepth);
Require(name, "D16 fallback depth/native depth copy",
std::memcmp(d16_native_words.data(), d16_fallback_values.data(),
sizeof(d16_fallback_values)) == 0 &&
texture_cache.GetImage(d16_native_image).backing.format ==
vk::Format::eD16Unorm,
"D16 depth lost its values when its target dropped the stencil");
const auto d16_restenciled_image =
texture_cache.FindImage(d16_stenciled_desc);
const auto d16_restenciled_words =
read_texels(d16_restenciled_image, vk::ImageAspectFlagBits::eDepth);
bool d16_promoted = d16_restenciled_image != d16_native_image;
for (size_t i = 0; i < d16_fallback_values.size(); ++i) {
d16_promoted &= (d16_restenciled_words[i] & 0x00ffffffu) ==
EncodeD16AsD24(d16_fallback_values[i]);
}
Require(name, "D16 native depth/fallback depth copy", d16_promoted,
"D16 depth lost its values when its target regained the stencil");
if (!texture_cache.GetImage(combined_destination_image).IsGpuModified()) {
texture_cache.MarkGpuWritten(combined_destination_image);
@@ -51982,94 +52023,6 @@ void CheckPm4IndirectControlFlow(RenderContext &renderer) {
std::printf("[host] %-32s ok\n", name);
}
// waitUntilSafeForRendering stalls only its own command processor, and a display buffer
// stays busy while its flip is pending or while it is on screen.
void CheckWaitUntilSafeForRendering() {
constexpr const char *name = "WaitUntilSafeForRendering";
Require(name, "video", SDL_InitSubSystem(SDL_INIT_VIDEO), "SDL video init failed");
WindowContext window;
window.graphic_ctx.screen_width = 64;
window.graphic_ctx.screen_height = 64;
window.window = SDL_CreateWindow(name, 64, 64, SDL_WINDOW_VULKAN | SDL_WINDOW_HIDDEN);
Require(name, "window", window.window != nullptr, "hidden Vulkan window failed");
window.CreateVulkan();
auto &video_out = VideoOut::VideoOutInit(64, 64, *window.presenter);
auto &renderer = *window.render_context;
renderer.InitializeGpu(&video_out);
auto &gpu = renderer.GetGpu();
constexpr int user_system = 255;
constexpr int bus_main = 0;
constexpr int flip_mode_vsync = 1;
// Special buffer indices flip without a registered surface.
constexpr int index_blank = -1;
constexpr int index_black = -2;
const int handle = VideoOut::VideoOutOpen(user_system, bus_main, 0, nullptr);
Require(name, "open", handle > 0, "video-out open failed");
const auto wait_until = [](const auto &done) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (!done() && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return done();
};
const auto address = [](const void *value) { return reinterpret_cast<uint64_t>(value); };
// Hold the graphics queue so the CPU flip of BLACK, prepared behind it, stays pending.
std::atomic_uint32_t gate{0};
std::array<uint32_t, 7> hold{};
hold[0] = KYTY_PM4(7, Pm4::IT_WAIT_REG_MEM, 0);
hold[1] = 0x10u | 3u;
hold[2] = static_cast<uint32_t>(address(&gate));
hold[3] = static_cast<uint32_t>(address(&gate) >> 32u);
hold[4] = 1;
hold[5] = UINT32_MAX;
gpu.Submit(hold);
Require(name, "flip black",
VideoOut::VideoOutSubmitFlip(handle, index_black, flip_mode_vsync, 0) == 0,
"CPU flip failed");
std::atomic_uint32_t rendered{0};
std::array<uint32_t, 12> render{};
render[0] = KYTY_PM4(7, Pm4::IT_NOP, Pm4::R_WAIT_FLIP_DONE);
render[1] = static_cast<uint32_t>(handle);
render[2] = static_cast<uint32_t>(index_black);
render[7] = KYTY_PM4(5, Pm4::IT_WRITE_DATA, 0);
render[9] = static_cast<uint32_t>(address(&rendered));
render[10] = static_cast<uint32_t>(address(&rendered) >> 32u);
render[11] = 1;
gpu.SubmitCompute(0x20, render);
std::binary_semaphore lane{0};
std::jthread command_lane([&] {
gpu.SendCommandSync([] {});
lane.release();
});
Require(name, "command lane", lane.try_acquire_for(std::chrono::seconds(2)),
"a busy display buffer blocked the GPU thread");
Require(name, "pending", rendered.load() == 0,
"rendering started while its display buffer had a pending flip");
gate.store(1);
Require(name, "flip shown", wait_until([handle] { return VideoOut::VideoOutIsFlipPending(handle) == 0; }),
"the BLACK flip was not presented");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Require(name, "displayed", rendered.load() == 0,
"rendering started while its display buffer was on screen");
Require(name, "flip blank",
VideoOut::VideoOutSubmitFlip(handle, index_blank, flip_mode_vsync, 0) == 0,
"CPU flip failed");
Require(name, "resume", wait_until([&] { return rendered.load() == 1; }),
"rendering did not resume once its display buffer left the screen");
gpu.WaitForIdle();
Require(name, "close", VideoOut::VideoOutClose(handle) == 0, "video-out close failed");
renderer.ShutdownGpu();
VideoOut::VideoOutShutdown();
std::printf("[host] %-32s ok\n", name);
}
// R_FLIP itself publishes the dumped occlusion counters, then flips.
void CheckPm4Flip() {
namespace EventQueue = LibKernel::EventQueue;
@@ -53105,10 +53058,6 @@ int main(int argc, char **argv) {
vulkan.CheckScalarLeafCall();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--wait-until-safe-for-rendering-only") == 0) {
CheckWaitUntilSafeForRendering();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--flip-only") == 0) {
CheckPm4Flip();
return 0;
@@ -53692,8 +53641,6 @@ int main(int argc, char **argv) {
vulkan.CheckGpuSuspendPoint();
vulkan.CheckGpuCommandLane();
vulkan.CheckScalarLeafCall();
// Last: its window installs global GPU resources.
CheckWaitUntilSafeForRendering();
if (skipped_device_checks) {
std::printf(
"ShaderRecompilerComputeTests: device rasterization checks skipped, this "
File diff suppressed because it is too large. Load diff