libretro: draw cursor in vulkan

This commit is contained in:
Eric Warmenhoven 2026-03-06 15:44:46 -05:00 committed by OpenSauce
parent 0407568006
commit 3a5fa35449
11 changed files with 285 additions and 44 deletions

View File

@ -626,6 +626,7 @@ bool retro_load_game(const struct retro_game_info* info) {
#endif
break;
case Settings::GraphicsAPI::Software:
emu_instance->emu_window->CreateContext();
emu_instance->game_loaded = do_load_game();
if (!emu_instance->game_loaded)
return false;

View File

@ -9,6 +9,7 @@
#include "audio_core/audio_types.h"
#include "citra_libretro/citra_libretro.h"
#include "citra_libretro/core_settings.h"
#include "citra_libretro/environment.h"
#include "citra_libretro/input/input_factory.h"
#include "common/settings.h"
@ -85,9 +86,7 @@ void EmuWindow_LibRetro::SwapBuffers() {
}
case Settings::GraphicsAPI::Vulkan: {
#ifdef ENABLE_VULKAN
if (enableEmulatedPointer && tracker) {
tracker->Render(width, height);
}
// Cursor is drawn inside the Vulkan render pass (RendererVulkan::DrawCursor)
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
static_cast<unsigned>(height), 0);
#endif
@ -340,3 +339,10 @@ void EmuWindow_LibRetro::CreateContext() {
void EmuWindow_LibRetro::DestroyContext() {
tracker = nullptr;
}
Frontend::EmuWindow::CursorInfo EmuWindow_LibRetro::GetCursorInfo() const {
if (enableEmulatedPointer && tracker && LibRetro::settings.render_touchscreen) {
return tracker->GetCursorInfo();
}
return {};
}

View File

@ -57,6 +57,9 @@ public:
/// When true, SwapBuffers() is suppressed (used during savestate drain loops)
bool suppressPresentation = false;
/// Get cursor state for rendering a touch crosshair on the bottom screen.
CursorInfo GetCursorInfo() const override;
private:
/// Called when a configuration change affects the minimal size of the window
void OnMinimalClientAreaChangeRequest(std::pair<u32, u32> minimal_size) override;

View File

@ -219,17 +219,9 @@ void MouseTracker::Update(int bufferWidth, int bufferHeight,
Restrict(0, 0, layout.bottom_screen.GetWidth(), layout.bottom_screen.GetHeight());
// Make the coordinates 0 -> 1
projectedX = (float)x / layout.bottom_screen.GetWidth();
projectedY = (float)y / layout.bottom_screen.GetHeight();
// Ensure that the projected position doesn't overlap outside the bottom screen framebuffer.
// TODO: Provide config option
renderRatio = (float)layout.bottom_screen.GetHeight() / 30;
// Map the mouse coord to the bottom screen's position
projectedX = layout.bottom_screen.left + projectedX * layout.bottom_screen.GetWidth();
projectedY = layout.bottom_screen.top + projectedY * layout.bottom_screen.GetHeight();
// Store as bottom-screen-local pixel coordinates
projectedX = static_cast<float>(x);
projectedY = static_cast<float>(y);
isPressed = state;
@ -241,10 +233,15 @@ void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_d
return;
}
// Delegate to renderer-specific implementation
// Delegate to renderer-specific implementation.
// Convert from bottom-screen-local to layout-absolute for the legacy renderers.
if (cursor_renderer) {
cursor_renderer->Render(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
framebuffer_layout, framebuffer_data);
const float abs_x = framebuffer_layout.bottom_screen.left + projectedX;
const float abs_y = framebuffer_layout.bottom_screen.top + projectedY;
const float ratio =
static_cast<float>(framebuffer_layout.bottom_screen.GetHeight()) / 30.0f;
cursor_renderer->Render(bufferWidth, bufferHeight, abs_x, abs_y, ratio, framebuffer_layout,
framebuffer_data);
}
}
@ -349,30 +346,12 @@ void OpenGLCursorRenderer::Render(int bufferWidth, int bufferHeight, float proje
#endif
#ifdef ENABLE_VULKAN
// Vulkan-specific cursor renderer implementation
VulkanCursorRenderer::VulkanCursorRenderer() {
// Vulkan cursor rendering will be integrated into the main rendering pipeline
}
// Vulkan cursor is drawn by RendererVulkan::DrawCursor() inside the render pass.
// This class exists only to satisfy the CursorRenderer interface.
VulkanCursorRenderer::VulkanCursorRenderer() = default;
VulkanCursorRenderer::~VulkanCursorRenderer() = default;
void VulkanCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX,
float projectedY, float renderRatio,
const Layout::FramebufferLayout& layout, void* framebuffer_data) {
// Use shared coordinate calculation
CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio,
layout);
// TODO: Implement actual Vulkan cursor drawing using the renderer's command buffer
// This would involve:
// 1. Creating a simple vertex buffer with cursor geometry using coords
// 2. Using a basic shader pipeline
// 3. Recording draw commands into the current command buffer
// 4. Using blend mode similar to OpenGL (ONE_MINUS_DST_COLOR, ONE_MINUS_SRC_COLOR)
// For now, this is a placeholder - the cursor won't be visible in Vulkan mode
// but the touchscreen input will still work
}
void VulkanCursorRenderer::Render(int, int, float, float, float, const Layout::FramebufferLayout&,
void*) {}
#endif
// Software-specific cursor renderer implementation

View File

@ -5,6 +5,7 @@
#pragma once
#include "common/math_util.h"
#include "core/frontend/emu_window.h"
#include "core/frontend/framebuffer_layout.h"
#ifdef ENABLE_OPENGL
@ -47,10 +48,15 @@ public:
return isPressed;
}
/// Get the pressed position, relative to the framebuffer.
/// Get the pressed position in layout-absolute coordinates.
std::pair<unsigned, unsigned> GetPressedPosition() {
return {static_cast<const unsigned int&>(projectedX),
static_cast<const unsigned int&>(projectedY)};
return {static_cast<unsigned>(framebuffer_layout.bottom_screen.left + projectedX),
static_cast<unsigned>(framebuffer_layout.bottom_screen.top + projectedY)};
}
/// Get cursor rendering state for external renderers (e.g. Vulkan).
Frontend::EmuWindow::CursorInfo GetCursorInfo() const {
return {true, projectedX, projectedY};
}
private:
@ -62,7 +68,6 @@ private:
float projectedX;
float projectedY;
float renderRatio;
bool isPressed;

View File

@ -269,6 +269,23 @@ public:
return true;
}
/// Cursor state for rendering a touch crosshair on the bottom screen.
struct CursorInfo {
bool visible = false;
/// Cursor position in bottom-screen-local pixel coordinates.
/// Origin is the top-left corner of the bottom screen, with x ranging
/// from 0 to bottom_screen.GetWidth() and y from 0 to
/// bottom_screen.GetHeight().
float projected_x = 0;
float projected_y = 0;
};
/// Returns the current cursor state. Override to provide cursor position.
virtual CursorInfo GetCursorInfo() const {
return {};
}
Settings::StereoRenderOption get3DMode() const;
protected:

View File

@ -24,6 +24,8 @@ set(SHADER_FILES
vulkan_present_anaglyph.frag
vulkan_present_interlaced.frag
vulkan_blit_depth_stencil.frag
vulkan_cursor.frag
vulkan_cursor.vert
)
set(SHADER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/include)

View File

@ -0,0 +1,12 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#version 450 core
#extension GL_ARB_separate_shader_objects : enable
layout (location = 0) out vec4 color;
void main() {
color = vec4(1.0, 1.0, 1.0, 1.0);
}

View File

@ -0,0 +1,12 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#version 450 core
#extension GL_ARB_separate_shader_objects : enable
layout (location = 0) in vec2 vert_position;
void main() {
gl_Position = vec4(vert_position, 0.0, 1.0);
}

View File

@ -20,6 +20,9 @@
#include "video_core/host_shaders/vulkan_present_interlaced_frag.h"
#include "video_core/host_shaders/vulkan_present_vert.h"
#include "video_core/host_shaders/vulkan_cursor_frag.h"
#include "video_core/host_shaders/vulkan_cursor_vert.h"
#include <vk_mem_alloc.h>
#if defined(__APPLE__) && !defined(HAVE_LIBRETRO)
@ -153,6 +156,10 @@ RendererVulkan::~RendererVulkan() {
device.destroyImageView(info.texture.image_view);
vmaDestroyImage(instance.GetAllocator(), info.texture.image, info.texture.allocation);
}
device.destroyPipeline(cursor_pipeline);
device.destroyShaderModule(cursor_vertex_shader);
device.destroyShaderModule(cursor_fragment_shader);
}
void RendererVulkan::PrepareRendertarget() {
@ -294,6 +301,11 @@ void RendererVulkan::CompileShaders() {
present_shaders[2] = Compile(HostShaders::VULKAN_PRESENT_INTERLACED_FRAG,
vk::ShaderStageFlagBits::eFragment, device, preamble);
cursor_vertex_shader =
Compile(HostShaders::VULKAN_CURSOR_VERT, vk::ShaderStageFlagBits::eVertex, device);
cursor_fragment_shader =
Compile(HostShaders::VULKAN_CURSOR_FRAG, vk::ShaderStageFlagBits::eFragment, device);
auto properties = instance.GetPhysicalDevice().getProperties();
for (std::size_t i = 0; i < present_samplers.size(); i++) {
const vk::Filter filter_mode = i == 0 ? vk::Filter::eLinear : vk::Filter::eNearest;
@ -330,6 +342,9 @@ void RendererVulkan::BuildLayouts() {
.pPushConstantRanges = &push_range,
};
present_pipeline_layout = instance.GetDevice().createPipelineLayoutUnique(layout_info);
const vk::PipelineLayoutCreateInfo cursor_layout_info = {};
cursor_pipeline_layout = instance.GetDevice().createPipelineLayoutUnique(cursor_layout_info);
}
void RendererVulkan::BuildPipelines() {
@ -460,6 +475,126 @@ void RendererVulkan::BuildPipelines() {
ASSERT_MSG(result == vk::Result::eSuccess, "Unable to build present pipelines");
present_pipelines[i] = pipeline;
}
// Build cursor pipeline (simple position-only, inverted color blending)
{
const vk::VertexInputBindingDescription cursor_binding = {
.binding = 0,
.stride = sizeof(float) * 2,
.inputRate = vk::VertexInputRate::eVertex,
};
const vk::VertexInputAttributeDescription cursor_attribute = {
.location = 0,
.binding = 0,
.format = vk::Format::eR32G32Sfloat,
.offset = 0,
};
const vk::PipelineVertexInputStateCreateInfo cursor_vertex_input = {
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &cursor_binding,
.vertexAttributeDescriptionCount = 1,
.pVertexAttributeDescriptions = &cursor_attribute,
};
const vk::PipelineInputAssemblyStateCreateInfo cursor_input_assembly = {
.topology = vk::PrimitiveTopology::eTriangleList,
.primitiveRestartEnable = false,
};
const vk::PipelineRasterizationStateCreateInfo cursor_raster = {
.depthClampEnable = false,
.rasterizerDiscardEnable = false,
.cullMode = vk::CullModeFlagBits::eNone,
.frontFace = vk::FrontFace::eClockwise,
.depthBiasEnable = false,
.lineWidth = 1.0f,
};
const vk::PipelineMultisampleStateCreateInfo cursor_multisample = {
.rasterizationSamples = vk::SampleCountFlagBits::e1,
.sampleShadingEnable = false,
};
const vk::PipelineColorBlendAttachmentState cursor_blend_attachment = {
.blendEnable = true,
.srcColorBlendFactor = vk::BlendFactor::eOneMinusDstColor,
.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcColor,
.colorBlendOp = vk::BlendOp::eAdd,
.srcAlphaBlendFactor = vk::BlendFactor::eOne,
.dstAlphaBlendFactor = vk::BlendFactor::eZero,
.alphaBlendOp = vk::BlendOp::eAdd,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
};
const vk::PipelineColorBlendStateCreateInfo cursor_color_blending = {
.logicOpEnable = false,
.attachmentCount = 1,
.pAttachments = &cursor_blend_attachment,
};
const vk::Viewport placeholder_vp = {0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
const vk::Rect2D placeholder_sc = {{0, 0}, {1, 1}};
const vk::PipelineViewportStateCreateInfo cursor_viewport = {
.viewportCount = 1,
.pViewports = &placeholder_vp,
.scissorCount = 1,
.pScissors = &placeholder_sc,
};
const std::array cursor_dynamic_states = {
vk::DynamicState::eViewport,
vk::DynamicState::eScissor,
};
const vk::PipelineDynamicStateCreateInfo cursor_dynamic = {
.dynamicStateCount = static_cast<u32>(cursor_dynamic_states.size()),
.pDynamicStates = cursor_dynamic_states.data(),
};
const vk::PipelineDepthStencilStateCreateInfo cursor_depth = {
.depthTestEnable = false,
.depthWriteEnable = false,
.depthCompareOp = vk::CompareOp::eAlways,
.depthBoundsTestEnable = false,
.stencilTestEnable = false,
};
const std::array cursor_shader_stages = {
vk::PipelineShaderStageCreateInfo{
.stage = vk::ShaderStageFlagBits::eVertex,
.module = cursor_vertex_shader,
.pName = "main",
},
vk::PipelineShaderStageCreateInfo{
.stage = vk::ShaderStageFlagBits::eFragment,
.module = cursor_fragment_shader,
.pName = "main",
},
};
const vk::GraphicsPipelineCreateInfo cursor_pipeline_info = {
.stageCount = static_cast<u32>(cursor_shader_stages.size()),
.pStages = cursor_shader_stages.data(),
.pVertexInputState = &cursor_vertex_input,
.pInputAssemblyState = &cursor_input_assembly,
.pViewportState = &cursor_viewport,
.pRasterizationState = &cursor_raster,
.pMultisampleState = &cursor_multisample,
.pDepthStencilState = &cursor_depth,
.pColorBlendState = &cursor_color_blending,
.pDynamicState = &cursor_dynamic,
.layout = *cursor_pipeline_layout,
.renderPass = main_present_window.Renderpass(),
};
const auto [result, pipeline] =
instance.GetDevice().createGraphicsPipeline({}, cursor_pipeline_info);
ASSERT_MSG(result == vk::Result::eSuccess, "Unable to build cursor pipeline");
cursor_pipeline = pipeline;
}
}
void RendererVulkan::ConfigureFramebufferTexture(TextureInfo& texture,
@ -909,9 +1044,71 @@ void RendererVulkan::DrawScreens(Frame* frame, const Layout::FramebufferLayout&
}
}
DrawCursor(layout);
scheduler.Record([](vk::CommandBuffer cmdbuf) { cmdbuf.endRenderPass(); });
}
void RendererVulkan::DrawCursor(const Layout::FramebufferLayout& layout) {
const auto cursor = render_window.GetCursorInfo();
if (!cursor.visible) {
return;
}
const float buf_w = static_cast<float>(layout.width);
const float buf_h = static_cast<float>(layout.height);
// Convert from bottom-screen-local to layout-absolute, then to NDC
const float abs_x = layout.bottom_screen.left + cursor.projected_x;
const float abs_y = layout.bottom_screen.top + cursor.projected_y;
const float cx = (abs_x / buf_w) * 2.0f - 1.0f;
const float cy = (abs_y / buf_h) * 2.0f - 1.0f;
const float ratio = static_cast<float>(layout.bottom_screen.GetHeight()) / 30.0f;
const float rw = ratio / buf_w;
const float rh = ratio / buf_h;
// Bottom screen bounds in NDC
const float bl = (layout.bottom_screen.left / buf_w) * 2.0f - 1.0f;
const float bt = (layout.bottom_screen.top / buf_h) * 2.0f - 1.0f;
const float br = (layout.bottom_screen.right / buf_w) * 2.0f - 1.0f;
const float bb = (layout.bottom_screen.bottom / buf_h) * 2.0f - 1.0f;
// Crosshair geometry clamped to bottom screen bounds
const float vl = std::fmax(cx - rw / 5.0f, bl);
const float vr = std::fmin(cx + rw / 5.0f, br);
const float vt = std::fmax(cy - rh, bt);
const float vb = std::fmin(cy + rh, bb);
const float hl = std::fmax(cx - rw, bl);
const float hr = std::fmin(cx + rw, br);
const float ht = std::fmax(cy - rh / 5.0f, bt);
const float hb = std::fmin(cy + rh / 5.0f, bb);
// 12 vertices = 4 triangles (2 for vertical bar, 2 for horizontal bar)
// clang-format off
const float vertices[] = {
// Vertical bar
vl, vt, vr, vt, vr, vb,
vl, vt, vr, vb, vl, vb,
// Horizontal bar
hl, ht, hr, ht, hr, hb,
hl, ht, hr, hb, hl, hb,
};
// clang-format on
const u64 size = sizeof(vertices);
auto [data, offset, invalidate] = vertex_buffer.Map(size, 16);
std::memcpy(data, vertices, size);
vertex_buffer.Commit(size);
scheduler.Record([this, offset = offset, pipeline = cursor_pipeline](vk::CommandBuffer cmdbuf) {
cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
cmdbuf.bindVertexBuffers(0, vertex_buffer.Handle(), {0});
const u32 first_vertex = static_cast<u32>(offset) / (sizeof(float) * 2);
cmdbuf.draw(12, 1, first_vertex, 0);
});
}
void RendererVulkan::SwapBuffers() {
system.perf_stats->StartSwap();
const Layout::FramebufferLayout& layout = render_window.GetFramebufferLayout();

View File

@ -112,6 +112,8 @@ private:
void ApplySecondLayerOpacity(float alpha);
void DrawCursor(const Layout::FramebufferLayout& layout);
void LoadFBToScreenInfo(const Pica::FramebufferConfig& framebuffer, ScreenInfo& screen_info,
bool right_eye);
void FillScreen(Common::Vec3<u8> color, const TextureInfo& texture);
@ -144,6 +146,11 @@ private:
std::array<ScreenInfo, 3> screen_infos{};
PresentUniformData draw_info{};
vk::ClearColorValue clear_color{};
vk::ShaderModule cursor_vertex_shader{};
vk::ShaderModule cursor_fragment_shader{};
vk::Pipeline cursor_pipeline{};
vk::UniquePipelineLayout cursor_pipeline_layout{};
};
} // namespace Vulkan