mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 08:13:05 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c01401862 | ||
|
|
a833e1b735 | ||
|
|
1be1c6c4f9 | ||
|
|
69fdb03dc2 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -866,9 +856,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
|
||||
// NUM_INSTANCES keeps the last draw's instance count.
|
||||
m_num_instances_addr = args_addr + static_cast<uint64_t>(draw_count - 1) * stride_in_bytes +
|
||||
offsetof(vk::DrawIndirectCommand, instanceCount);
|
||||
// The GPU reads the records if the shaders allow; the VS prolog loads the patched SGPRs from
|
||||
// Vulkan's bases. With no SGPR patched, Vulkan applies the offsets, which matches while they
|
||||
// are zero.
|
||||
// The GPU reads the records if the shaders allow; the VS prolog removes Vulkan's bases
|
||||
// from the index VGPRs and copies them only to the user SGPRs the packet patches.
|
||||
DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
|
||||
if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) &&
|
||||
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
|
||||
@@ -960,11 +949,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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -19,7 +19,13 @@ namespace Libs::Graphics {
|
||||
|
||||
enum class ColorTransform : uint32_t { None, SwapBgra16, Reverse10_11_11 };
|
||||
|
||||
enum class VideoOutCompression : uint8_t { Uncompressed, Dcc256_256_0, Dcc256_64_64, Unsupported };
|
||||
enum class VideoOutCompression : uint8_t {
|
||||
Uncompressed,
|
||||
Dcc256_256_0,
|
||||
Dcc256_64_64,
|
||||
Dcc128_128_128,
|
||||
Unsupported
|
||||
};
|
||||
|
||||
enum class ImageMetadataKind : uint8_t { None, Htile, Dcc, Cmask };
|
||||
|
||||
@@ -287,8 +293,9 @@ inline bool ImageInfo::IsDepth() const noexcept {
|
||||
[[nodiscard]] inline VideoOutCompression
|
||||
ClassifyVideoOutCompression(bool compressed, uint64_t metadata_address, uint32_t dcc_control,
|
||||
uint64_t dcc_clear_color) noexcept {
|
||||
constexpr uint32_t DCC_256_256_0 = 0x00000048u;
|
||||
constexpr uint32_t DCC_256_64_64 = 0x00000208u;
|
||||
constexpr uint32_t DCC_256_256_0 = 0x00000048u;
|
||||
constexpr uint32_t DCC_256_64_64 = 0x00000208u;
|
||||
constexpr uint32_t DCC_128_128_128 = 0x00100024u;
|
||||
if (!compressed) {
|
||||
return metadata_address == 0 && dcc_control == 0 && dcc_clear_color == 0
|
||||
? VideoOutCompression::Uncompressed
|
||||
@@ -300,6 +307,7 @@ ClassifyVideoOutCompression(bool compressed, uint64_t metadata_address, uint32_t
|
||||
switch (dcc_control) {
|
||||
case DCC_256_256_0: return VideoOutCompression::Dcc256_256_0;
|
||||
case DCC_256_64_64: return VideoOutCompression::Dcc256_64_64;
|
||||
case DCC_128_128_128: return VideoOutCompression::Dcc128_128_128;
|
||||
default: return VideoOutCompression::Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,6 +689,7 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
|
||||
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh;
|
||||
if (!tess_active && !mesh_active) {
|
||||
vertex_info[0].indirect_offsets = state.indirect_offsets;
|
||||
vertex_info[0].indirect_draw = state.indirect_draw;
|
||||
}
|
||||
if (object_primitive_id) {
|
||||
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
|
||||
|
||||
@@ -139,6 +139,7 @@ struct DrawRenderState {
|
||||
ShaderPixelInputInfo ps_input_info;
|
||||
// Input: the user-data slots a GPU-read indirect draw patches.
|
||||
ShaderIndirectOffsets indirect_offsets;
|
||||
bool indirect_draw = false;
|
||||
|
||||
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
|
||||
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
|
||||
|
||||
@@ -52,7 +52,7 @@ std::pair<int32_t, uint32_t> ResolveDrawOffsets(uint32_t index_offset,
|
||||
const ShaderVertexInputInfo& vs_input_info) {
|
||||
auto vertex_offset = static_cast<int32_t>(index_offset);
|
||||
uint32_t instance_offset = 0;
|
||||
if (!vs_input_info.fetch_embedded) {
|
||||
if (!vs_input_info.has_dynamic_vertex_fetch) {
|
||||
return {vertex_offset, instance_offset};
|
||||
}
|
||||
|
||||
@@ -1251,16 +1251,26 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
}
|
||||
|
||||
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh
|
||||
// and quad-list draws for counts, tessellation draws for patched SGPRs. Indirect records carry
|
||||
// their own offsets.
|
||||
// and quad-list draws for counts, tessellation draws for the index/SGPR prolog. Indirect records
|
||||
// carry their own offsets.
|
||||
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
|
||||
const ShaderVertexInputInfo& vs,
|
||||
DrawOffsetSource source,
|
||||
int32_t first_vertex,
|
||||
uint32_t first_instance,
|
||||
const DrawIndirectArgs& indirect) {
|
||||
if ((indirect.address != 0 || source == DrawOffsetSource::IndirectArgs) &&
|
||||
ucfg.GetIndexOffset() != 0) {
|
||||
static std::once_flag warning_once;
|
||||
std::call_once(warning_once, [&] {
|
||||
Log::WriteToConsoleAndLog(fmt::format(
|
||||
"GPU: indirect draw ignores GE_INDX_OFFSET=0x{:08x}; geometry may be incorrect.\n",
|
||||
ucfg.GetIndexOffset()));
|
||||
});
|
||||
}
|
||||
if (indirect.address != 0) {
|
||||
if (vs.stage.module->info.stage == ShaderType::Mesh ||
|
||||
if (!vs.indirect_draw ||
|
||||
vs.stage.module->info.stage == ShaderType::Mesh ||
|
||||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy ||
|
||||
vs.indirect_offsets != indirect.offsets) {
|
||||
return std::nullopt;
|
||||
@@ -1269,7 +1279,7 @@ static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig&
|
||||
}
|
||||
if (source == DrawOffsetSource::IndirectArgs) {
|
||||
// Native fetches consume the patched SGPRs; rewritten fetches need Vulkan offsets.
|
||||
return vs.fetch_embedded ? DrawEmitInfo {first_vertex, first_instance} : DrawEmitInfo {};
|
||||
return vs.has_dynamic_vertex_fetch ? DrawEmitInfo {first_vertex, first_instance} : DrawEmitInfo {};
|
||||
}
|
||||
const auto [vertex_offset, instance_offset] = ResolveDrawOffsets(ucfg.GetIndexOffset(), vs);
|
||||
return DrawEmitInfo {vertex_offset + first_vertex, instance_offset};
|
||||
@@ -1326,7 +1336,8 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
hw_check(buffer);
|
||||
|
||||
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
|
||||
DrawRenderState state {.indirect_offsets = args.indirect.offsets,
|
||||
.indirect_draw = args.indirect.address != 0};
|
||||
if (!GetDrawTopology(ucfg, state.topology)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1431,7 +1442,8 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
||||
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
|
||||
args.vertex_count, args.instance_count, args.first_instance};
|
||||
|
||||
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
|
||||
DrawRenderState state {.indirect_offsets = args.indirect.offsets,
|
||||
.indirect_draw = args.indirect.address != 0};
|
||||
if (!GetDrawTopology(ucfg, state.topology)) {
|
||||
ResetBindings();
|
||||
return true;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
®ion, 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;
|
||||
|
||||
@@ -814,7 +814,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
||||
const auto& cfg = *selected_cfg;
|
||||
Frontend::EmbeddedFetchPlan embedded_fetch;
|
||||
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
|
||||
options.input_info.vertex->fetch_embedded) {
|
||||
options.input_info.vertex->has_dynamic_vertex_fetch) {
|
||||
embedded_fetch = DetectEmbeddedVertexFetch(
|
||||
decoded, *options.input_info.vertex, options.user_data_base,
|
||||
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
|
||||
|
||||
@@ -1390,11 +1390,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
|
||||
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
|
||||
// Vulkan adds an indirect record's bases to the indices; the hardware passes them
|
||||
// only in the user SGPRs the packet patches.
|
||||
// only in the user SGPRs the packet patches. SH_NOP leaves those SGPRs unchanged;
|
||||
// it does not make the record's bases zero.
|
||||
const auto& offsets = options.input_info.vertex->indirect_offsets;
|
||||
const auto index = [&](uint8_t slot, IR::StageInputKind kind,
|
||||
IR::StageInputKind base_kind) {
|
||||
if (offsets == ShaderIndirectOffsets {}) {
|
||||
if (!options.input_info.vertex->indirect_draw) {
|
||||
return builtin(kind);
|
||||
}
|
||||
const auto base = builtin(base_kind);
|
||||
|
||||
@@ -291,10 +291,9 @@ static bool ShaderGetStaticVertexInputInfo(uint64_t shader_addr, const HW::UserS
|
||||
const auto& metadata = data.vertex;
|
||||
|
||||
if (metadata.vertex_attrib_reg >= 0) {
|
||||
info.fetch_external = false;
|
||||
info.fetch_embedded = true;
|
||||
info.fetch_attrib_reg = metadata.vertex_attrib_reg;
|
||||
info.fetch_buffer_reg = metadata.vertex_buffer_reg;
|
||||
info.has_dynamic_vertex_fetch = true;
|
||||
info.fetch_attrib_reg = metadata.vertex_attrib_reg;
|
||||
info.fetch_buffer_reg = metadata.vertex_buffer_reg;
|
||||
|
||||
const auto* attrib = reinterpret_cast<const uint32_t*>(
|
||||
static_cast<uint64_t>(user_sgpr.value[metadata.vertex_attrib_reg]) |
|
||||
@@ -413,7 +412,7 @@ static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
|
||||
void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t>& key) {
|
||||
EXIT_IF(info.resources_num < 0 || info.resources_num > ShaderVertexInputInfo::RES_MAX);
|
||||
key.clear();
|
||||
key.push_back(static_cast<uint32_t>(info.fetch_embedded));
|
||||
key.push_back(static_cast<uint32_t>(info.has_dynamic_vertex_fetch));
|
||||
key.push_back(static_cast<uint32_t>(info.fetch_attrib_reg));
|
||||
key.push_back(static_cast<uint32_t>(info.fetch_buffer_reg));
|
||||
key.push_back(info.resources_num);
|
||||
@@ -422,7 +421,8 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
|
||||
key.push_back(info.lds_size_dwords);
|
||||
key.push_back(info.pa_cl_vs_out_cntl);
|
||||
key.push_back(info.user_clip_planes);
|
||||
key.push_back(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u));
|
||||
key.push_back(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u) |
|
||||
(uint32_t {info.indirect_draw} << 16u));
|
||||
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
|
||||
if (info.clip_space.enabled) {
|
||||
for (const float value: info.clip_space.scale) {
|
||||
@@ -693,9 +693,7 @@ void ShaderDbgDumpInputInfo(const ShaderVertexInputInfo& info) {
|
||||
|
||||
LOGF("ShaderDbgDumpInputInfo()\n");
|
||||
|
||||
LOGF("\t fetch_external = %s\n"
|
||||
"\t fetch_embedded = %s\n",
|
||||
info.fetch_external ? "true" : "false", info.fetch_embedded ? "true" : "false");
|
||||
LOGF("\t has_dynamic_vertex_fetch = %s\n", info.has_dynamic_vertex_fetch ? "true" : "false");
|
||||
|
||||
for (int i = 0; i < info.resources_num; i++) {
|
||||
LOGF("\t input %d\n", i);
|
||||
|
||||
@@ -125,8 +125,7 @@ struct ShaderTessellationInputInfo {
|
||||
};
|
||||
|
||||
// GS user-data slots an indirect draw packet writes its record's base vertex and first instance
|
||||
// to. Like the hardware index VGPRs, the vertex shader's indices exclude both bases once either
|
||||
// slot is set.
|
||||
// to. A packet can leave both slots unused even when its record contains nonzero offsets.
|
||||
struct ShaderIndirectOffsets {
|
||||
static constexpr uint8_t None = UINT8_MAX;
|
||||
|
||||
@@ -159,8 +158,10 @@ struct ShaderVertexInputInfo {
|
||||
ShaderMeshInputInfo mesh;
|
||||
ShaderTessellationInputInfo tess;
|
||||
ShaderIndirectOffsets indirect_offsets;
|
||||
bool fetch_external = false;
|
||||
bool fetch_embedded = false;
|
||||
// GPU-read records add Vulkan bases even when the packet patches no user SGPRs.
|
||||
bool indirect_draw = false;
|
||||
// Attribute-table metadata enables dynamic fetch detection; loads are matched later.
|
||||
bool has_dynamic_vertex_fetch = false;
|
||||
};
|
||||
|
||||
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -19739,9 +19739,9 @@ public:
|
||||
"the draw's cull mode or front face did not reach the host GPU");
|
||||
registers.SetModeControl({});
|
||||
|
||||
// Indirect records come from the GPU copy; the CPU backing holds zeros. In each pair the
|
||||
// first record draws nothing and the second covers the target, through vertex offset 7
|
||||
// for embedded fetches.
|
||||
// Indirect records come from the GPU copy; the CPU backing holds zeros. Embedded
|
||||
// fetches use vertex offset 7. Native shaders receive raw vertex/instance IDs when
|
||||
// the packet does not patch either offset SGPR, even with nonzero record offsets.
|
||||
{
|
||||
constexpr uint64_t vertices_address = depth_address + 0x38000;
|
||||
constexpr uint64_t args_address = vertices_address + 0x100;
|
||||
@@ -19752,19 +19752,23 @@ public:
|
||||
sizeof(positions));
|
||||
constexpr uint32_t stride = 32;
|
||||
const vk::DrawIndexedIndirectCommand indexed_records[]{
|
||||
{3, 1, 0, 0, 5}, {3, 1, 0, 7, 5}, {0, 1, 0, 0, 0}, {3, 1, 0, 0, 0}};
|
||||
{3, 1, 0, 0, 5}, {3, 1, 0, 7, 5}, {0, 1, 0, 0, 0}, {3, 1, 0, 0, 0},
|
||||
{0, 1, 0, 0, 0}, {3, 1, 0, -7, 0}, {0, 1, 0, 0, 0}, {3, 1, 0, 0, 11}};
|
||||
const vk::DrawIndirectCommand auto_records[]{{3, 1, 0, 5}, {3, 1, 7, 5}, {0, 1, 0, 0},
|
||||
{3, 1, 0, 0}};
|
||||
for (u32 i = 0; i < 4; i++) {
|
||||
{3, 1, 0, 0}, {0, 1, 0, 0}, {3, 1, 13, 0},
|
||||
{0, 1, 0, 0}, {3, 1, 0, 11}};
|
||||
constexpr u32 records_per_kind = std::size(indexed_records);
|
||||
static_assert(records_per_kind == std::size(auto_records));
|
||||
for (u32 i = 0; i < records_per_kind; i++) {
|
||||
std::memcpy(reinterpret_cast<void *>(args_address + i * stride), &indexed_records[i],
|
||||
sizeof(indexed_records[i]));
|
||||
std::memcpy(reinterpret_cast<void *>(args_address + (4 + i) * stride), &auto_records[i],
|
||||
sizeof(auto_records[i]));
|
||||
std::memcpy(reinterpret_cast<void *>(args_address + (records_per_kind + i) * stride),
|
||||
&auto_records[i], sizeof(auto_records[i]));
|
||||
}
|
||||
// Larger than a caching page, so the arguments are cached instead of streamed.
|
||||
(void)context.GetBufferCache().ObtainBuffer(
|
||||
vertices_address, BufferCache::CACHING_PAGESIZE * 2, false);
|
||||
const std::array<u32, 8 * stride / 4> zeros{};
|
||||
const std::array<u32, 2 * records_per_kind * stride / 4> zeros{};
|
||||
Require("GpuIndirectDraw", "CPU backing",
|
||||
LibKernel::Memory::TryWriteBacking(args_address, zeros.data(), sizeof(zeros)),
|
||||
"the CPU copy of the arguments could not be cleared");
|
||||
@@ -19825,26 +19829,35 @@ public:
|
||||
}
|
||||
};
|
||||
check("indexed embedded fetch", fetch_vs, true, 0);
|
||||
check("auto embedded fetch", fetch_vs, false, 4);
|
||||
check("auto embedded fetch", fetch_vs, false, records_per_kind);
|
||||
check("indexed native fetch", vertex_address, true, 2);
|
||||
check("auto native fetch", vertex_address, false, 6);
|
||||
check("auto native fetch", vertex_address, false, records_per_kind + 2);
|
||||
// Pipelines created from only their key and stage modules draw the same.
|
||||
scheduler.Finish();
|
||||
Require("PipelineFromKey", "vertex input pipelines",
|
||||
PipelineCacheTestAccess::RecreateGraphicsPipelines(context.GetPipelineCache()) > 0,
|
||||
"no cached graphics pipeline named only compiled modules");
|
||||
check("recreated indexed embedded fetch", fetch_vs, true, 0);
|
||||
check("recreated auto native fetch", vertex_address, false, 6);
|
||||
check("recreated auto native fetch", vertex_address, false, records_per_kind + 2);
|
||||
// Patched user SGPRs 0/1 read the records' base vertex 7 and first instance 5, not the
|
||||
// table pointers they hold, and the index VGPRs exclude both bases.
|
||||
check("indexed patched offsets", indirect_vs, true, 0, {0, 1});
|
||||
check("auto patched offsets", indirect_vs, false, 4, {0, 1});
|
||||
check("auto patched offsets", indirect_vs, false, records_per_kind, {0, 1});
|
||||
// Slots 4/5 lie past USER_SGPR, so the tables survive; rewritten fetches keep Vulkan's
|
||||
// vertex offset.
|
||||
check("indexed patched embedded fetch", fetch_vs, true, 0, {4, 5});
|
||||
// Patching one offset still leaves both bases out of the index VGPRs.
|
||||
check("auto instance-only patch", vertex_address, false, 5,
|
||||
check("auto instance-only patch", vertex_address, false, records_per_kind + 1,
|
||||
{ShaderIndirectOffsets::None, 1});
|
||||
// SH_NOP leaves the existing SGPR values intact; the native prolog adds these
|
||||
// values once while the record's unrelated offsets are ignored. Check each base
|
||||
// independently so a correct vertex offset cannot hide a broken instance offset.
|
||||
shaders.SetGsUserSgpr(0, 7);
|
||||
shaders.SetGsUserSgpr(1, 5);
|
||||
check("indexed unpatched vertex offset", indirect_vs, true, 4);
|
||||
check("auto unpatched vertex offset", indirect_vs, false, records_per_kind + 4);
|
||||
check("indexed unpatched instance offset", indirect_vs, true, 6);
|
||||
check("auto unpatched instance offset", indirect_vs, false, records_per_kind + 6);
|
||||
}
|
||||
|
||||
// A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.
|
||||
@@ -47399,7 +47412,7 @@ void CheckEmbeddedFetchVertexOffset() {
|
||||
vertex.tess = {.input_control_points = 3, .output_control_points = 3,
|
||||
.ls_stride = 124, .hs_stride = 128, .domain = 1,
|
||||
.partitioning = 2, .output_topology = 2};
|
||||
vertex.fetch_embedded = true;
|
||||
vertex.has_dynamic_vertex_fetch = true;
|
||||
vertex.fetch_buffer_reg = 0;
|
||||
vertex.fetch_attrib_reg = 2;
|
||||
vertex.resources_num = 1;
|
||||
@@ -47439,7 +47452,7 @@ void CheckEmbeddedFetchVertexOffset() {
|
||||
const auto Resolve = [](const CompiledShader &result,
|
||||
u32 index_offset) {
|
||||
ShaderVertexInputInfo vertex;
|
||||
vertex.fetch_embedded = true;
|
||||
vertex.has_dynamic_vertex_fetch = true;
|
||||
ShaderModule module;
|
||||
module.info.user_data_base = result.program.user_data_base;
|
||||
module.info.info.vertex_offset_sgpr = result.program.info.vertex_offset_sgpr;
|
||||
@@ -47450,7 +47463,7 @@ void CheckEmbeddedFetchVertexOffset() {
|
||||
|
||||
const auto ResolveInstance = [](const CompiledShader &result) {
|
||||
ShaderVertexInputInfo vertex;
|
||||
vertex.fetch_embedded = true;
|
||||
vertex.has_dynamic_vertex_fetch = true;
|
||||
ShaderModule module;
|
||||
module.info.user_data_base = result.program.user_data_base;
|
||||
module.info.info.instance_offset_sgpr =
|
||||
@@ -50344,7 +50357,7 @@ void CheckEmbeddedFetchLaneSpill() {
|
||||
|
||||
std::array<u32, 11> user_data{};
|
||||
ShaderVertexInputInfo vertex;
|
||||
vertex.fetch_embedded = true;
|
||||
vertex.has_dynamic_vertex_fetch = true;
|
||||
vertex.fetch_buffer_reg = 0;
|
||||
vertex.resources_num = 1;
|
||||
vertex.resources[0].fields[3] =
|
||||
@@ -52023,94 +52036,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;
|
||||
@@ -53146,10 +53071,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;
|
||||
@@ -53733,8 +53654,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 "
|
||||
|
||||
@@ -1938,7 +1938,7 @@ void TestVertexBufferGrouping() {
|
||||
Check(input.fetch_attrib_reg == 2, "a draw reread the vertex metadata of a mapped shader");
|
||||
ShaderMapUserData(regs.es_regs.data_addr, mapped);
|
||||
(void)PrepareProgram(regs, HW::Context{}, HW::UserConfig{}, input);
|
||||
Check(!input.fetch_embedded, "remapping a shader kept its previous vertex metadata");
|
||||
Check(!input.has_dynamic_vertex_fetch, "remapping a shader kept its previous vertex metadata");
|
||||
}
|
||||
|
||||
void TestNggVertexEntryState() {
|
||||
@@ -11575,7 +11575,7 @@ void TestEmbeddedFetchPreservesSharedScalarLoad() {
|
||||
reinterpret_cast<uint64_t>(attributes.data())};
|
||||
std::memcpy(user_data.data() + 12, tables, sizeof(tables));
|
||||
ShaderVertexInputInfo input{};
|
||||
input.fetch_embedded = true;
|
||||
input.has_dynamic_vertex_fetch = true;
|
||||
input.fetch_buffer_reg = 12;
|
||||
input.fetch_attrib_reg = 14;
|
||||
input.resources_num = 1;
|
||||
|
||||
Reference in new issue
Block a user