mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Match Azahar's stylus to DeSmuME
Replace Azahar's inverted cursor in Vulkan and software rendering with the
same supersampled white pen, dark outline and lower-left nib as DeSmuME.
Keep clipping anchored to the touch point and preserve the input behavior.
Verify: tools/verify.sh format
PS5_FRONTENDS='es-de picker' PS5_VULKAN_TEMPLATE="$PWD/build/picker-template" tools/verify.sh unit build integration evidence
182 unit and 182 integration tests pass; native core ABI/provenance pass.
The parity regression fails on the old cursor and passes at 1x, 5x and 18x.
Evidence: evidence/azahar-stylus-parity/{capture,expectation}.json
Fresh base-PS5 launch draws the stylus in both captured frames and exits
cleanly; stored eboot and DS/3DS core hashes match the staged package.
105 evidence records replay successfully. Core-switch crash is unresolved.
This commit is contained in:
1 parent
63e11a3db0
commit
ab4666cc5f
4 files changed
+324
-54
No files matched your search
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"step": "azahar-stylus-parity",
|
||||
"recorded": "2026-10-08T00:25:07.482933+00:00",
|
||||
"revision": "working tree on 63e11a3",
|
||||
"command": "tools/verify.sh format; PS5_FRONTENDS=\"es-de picker\" PS5_VULKAN_TEMPLATE=\"$PWD/build/picker-template\" tools/verify.sh unit build integration evidence; python3 tools/deploy-title.py",
|
||||
"lines": [
|
||||
"format: PASS",
|
||||
"182 unit and 182 integration tests: PASS",
|
||||
"Azahar software raster matches DeSmuME at 1x, 5x and 18x: PASS",
|
||||
"Vulkan shader compile/link, pen predicate parity and clipped nib coordinates: PASS",
|
||||
"Old Azahar raster fails the DeSmuME pixel comparison as expected",
|
||||
"Native Azahar build, ABI and cursor-patch provenance: PASS",
|
||||
"Stored eboot.bin and both cores match staged SHA-256: PASS",
|
||||
"Core-switch crash remains unresolved; no lifecycle changes made",
|
||||
"Fresh PS5 Azahar launch: cursor captured, 399 frames by 7 seconds, clean exit"
|
||||
],
|
||||
"native_azahar": {
|
||||
"sha256": "54cdcf809cf521eb544a60c26726a3074045c812c53942ce3cb9b4b383b2b0f6",
|
||||
"bytes": 62186560,
|
||||
"abi_passed": true,
|
||||
"source_revision": "4aef900a1a903d0aa6ea86fdb1e18236fb0d5907",
|
||||
"port_inputs_sha256": {
|
||||
"tools/core-fork.sh": "392c8a7577eaa7a81e99f58a6e006cecab92610d959dc460aabc3ef1064ac1b0",
|
||||
"tooling/native/ps5-core.ld": "5ffe0f0e99e23de5245afb46fa6dfda6dd860b53d942a5fe2f06eaf9e520df42",
|
||||
"tooling/native/core_cxx_runtime.cpp": "6d7c11d8595f47601b3a281e616033ea5d28c6329872aa286d4d81c597509433",
|
||||
"tools/build-azahar.sh": "2418b991f7f4a71c4c4c984ab7831dad085861c6a40e82c0e8ca791acee2f40c",
|
||||
"tooling/azahar/ps5-toolchain.cmake": "930762cf6f56d1e8b1a927bd70ac5e782f77cdb9b9622617973ef92ce881c398",
|
||||
"tooling/azahar/touchpad-stylus.patch": "fa8187c2919fda1152b611318aaeced29f9c4e8ccb25997f25f230ce624ccd6f"
|
||||
}
|
||||
},
|
||||
"deployed_readback": {
|
||||
"eboot.bin": {
|
||||
"bytes": 38772204,
|
||||
"sha256": "096ba6d77e9a6667f0d41c1473268200316cd086ee6b4d83523ada646e255287",
|
||||
"matches_staged": true
|
||||
},
|
||||
"cores/desmume_libretro.so": {
|
||||
"bytes": 5626312,
|
||||
"sha256": "b1b4d20bf62a4115007646b76ae7f5e04ddb661b8f9cb5b79a00c940c3b801a4",
|
||||
"matches_staged": true
|
||||
},
|
||||
"cores/azahar_libretro.so": {
|
||||
"bytes": 62186560,
|
||||
"sha256": "54cdcf809cf521eb544a60c26726a3074045c812c53942ce3cb9b4b383b2b0f6",
|
||||
"matches_staged": true
|
||||
}
|
||||
},
|
||||
"preview_sha256": "d7d3b09c0ad154398920b1f614a9d5c3cb4dec9dd71156197ea995226ca15a84",
|
||||
"raw_directory": "klog/azahar-stylus-parity",
|
||||
"console_run": {
|
||||
"command": "python3 klog/azahar-stylus-parity/run-fresh.py",
|
||||
"build_id": "cbe6d6f13b15aaaf5f75604e555562a32f5dce39ef0a434e84a661dee6a2ae83",
|
||||
"raw_capture": "klog/run-PPSA99169-202311.log",
|
||||
"frontend_log": "klog/retroarch-202311.log",
|
||||
"events": [
|
||||
"input: pad script MARK at 7.011 s: frames=399 worst_ms=80.4 over_40ms=7",
|
||||
"input: pad script STOP at 9.01 s",
|
||||
"rarch_main returned = 0"
|
||||
],
|
||||
"frames": [
|
||||
{
|
||||
"path": "klog/pad-202311/pad-shot-1.png",
|
||||
"sha256": "54eaff3ddb6428207c2760e30c39f02b53c1244242c08080ae1eb43f3a6a8efb"
|
||||
},
|
||||
{
|
||||
"path": "klog/pad-202311/pad-shot-2.png",
|
||||
"sha256": "cc1920a1ad0e2a7337099e11b4ed18144faf232c292ee6443bdc6d29ef327361"
|
||||
}
|
||||
],
|
||||
"visual_check": "Both screenshots show the white pen with dark outline/nib pointing down-left on the lower screen."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"step": "azahar-stylus-parity",
|
||||
"note": "Matching DeSmuME pen in Azahar; crash investigation is separate.",
|
||||
"must_contain": [
|
||||
"format: PASS",
|
||||
"182 unit and 182 integration tests: PASS",
|
||||
"Azahar software raster matches DeSmuME at 1x, 5x and 18x: PASS",
|
||||
"Vulkan shader compile/link, pen predicate parity and clipped nib coordinates: PASS",
|
||||
"Old Azahar raster fails the DeSmuME pixel comparison as expected",
|
||||
"Native Azahar build, ABI and cursor-patch provenance: PASS",
|
||||
"Stored eboot.bin and both cores match staged SHA-256: PASS",
|
||||
"Core-switch crash remains unresolved; no lifecycle changes made",
|
||||
"Fresh PS5 Azahar launch: cursor captured, 399 frames by 7 seconds, clean exit"
|
||||
],
|
||||
"must_not_contain": [],
|
||||
"human_check": "Check the stylus with physical touchpad movement, press and drag on the TV."
|
||||
}
|
||||
@@ -127,32 +127,83 @@ int main() {
|
||||
}
|
||||
''')
|
||||
|
||||
def test_azahar_software_stylus(self):
|
||||
source = self.patched('azahar', 'src/citra_libretro/input/mouse_tracker.cpp')
|
||||
def test_azahar_stylus_matches_desmume(self):
|
||||
software = self.patched('azahar', 'src/citra_libretro/input/mouse_tracker.cpp')
|
||||
desmume = self.patched('desmume', 'desmume/src/frontend/libretro/libretro.cpp')
|
||||
shader = self.patched('azahar', 'src/video_core/host_shaders/vulkan_cursor.frag')
|
||||
# Compile the real shaders together, then exercise the fragment's actual
|
||||
# shape predicate against DeSmuME's rasterizer at native and scaled sizes.
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
for ext in ('vert', 'frag'):
|
||||
Path(td, f'cursor.{ext}').write_text(self.patched(
|
||||
'azahar', f'src/video_core/host_shaders/vulkan_cursor.{ext}'))
|
||||
subprocess.run(['glslangValidator', '-V', '-l', 'cursor.vert', 'cursor.frag'],
|
||||
cwd=td, check=True, capture_output=True)
|
||||
shape = shader[shader.index(' float along'):shader.index(' color +=')]
|
||||
shape = shape.replace('p.x', 'dx').replace('p.y', 'dy')
|
||||
shape = shape.replace('vec3 ink', 'uint32_t ink').replace('vec3(1.0)', '0xffffff')
|
||||
shape = shape.replace('vec3(32.0, 40.0, 48.0) / 255.0', '0x202830')
|
||||
# GLSL literals are floats; preserve that in the extracted C++ predicate.
|
||||
import re
|
||||
shape = re.sub(r'(\d+\.\d+)', r'\1f', shape)
|
||||
self.compile_run("""
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
using std::min; using std::max; using std::abs;
|
||||
constexpr int RETRO_PIXEL_FORMAT_XRGB8888=1;
|
||||
int colorMode=1;
|
||||
uint32_t pointer_color_32=0xffffff;
|
||||
namespace Layout {
|
||||
struct FramebufferLayout {
|
||||
bool IsWithinTouchscreen(int x,int y)const{return x>=10&&x<30&&y>=20&&y<40;}
|
||||
int left,top,right,bottom;
|
||||
bool IsWithinTouchscreen(int x,int y)const{return x>=left&&x<right&&y>=top&&y<bottom;}
|
||||
}; }
|
||||
struct SoftwareCursorRenderer {
|
||||
void Render(int,int,float,float,float,const Layout::FramebufferLayout&,void*);
|
||||
};
|
||||
""" + function(source, 'void SoftwareCursorRenderer::Render(') + """
|
||||
uint32_t shader_sample(float dx,float dy,uint32_t original) {
|
||||
""" + shape + """
|
||||
return ink;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
""" + function(desmume, 'static void DrawStylus(') +
|
||||
function(software, 'void SoftwareCursorRenderer::Render(') + """
|
||||
int main() {
|
||||
SoftwareCursorRenderer cursor; Layout::FramebufferLayout layout;
|
||||
for(float x : {10.f,29.f}) for(float y : {20.f,39.f}) {
|
||||
std::vector<uint32_t> pixels(40*50,0xff123456);
|
||||
cursor.Render(40,50,x,y,8,layout,pixels.data());
|
||||
assert(pixels[static_cast<int>(y)*40+static_cast<int>(x)]==0xffedcba9);
|
||||
for(int py=0;py<50;++py) for(int px=0;px<40;++px)
|
||||
if(!layout.IsWithinTouchscreen(px,py)) assert(pixels[py*40+px]==0xff123456);
|
||||
SoftwareCursorRenderer cursor;
|
||||
for(int scale : {1,5,18}) {
|
||||
const int w=40*scale,h=32*scale;
|
||||
Layout::FramebufferLayout layout{0,0,w,h};
|
||||
for(int x : {0,w/2,w-1}) for(int y : {0,h/2,h-1}) {
|
||||
std::vector<uint32_t> ds(w*h,0x123456),az=ds;
|
||||
DrawStylus(reinterpret_cast<uint16_t*>(ds.data()),w,w,h,x,y,scale);
|
||||
cursor.Render(w,h,x,y,8*scale,layout,az.data());
|
||||
assert(ds==az);
|
||||
for(int py=0;py<h;++py) for(int px=0;px<w;++px) {
|
||||
unsigned r=0,g=0,b=0;
|
||||
for(int sy=0;sy<2;++sy) for(int sx=0;sx<2;++sx) {
|
||||
auto c=shader_sample((px-x+(sx-.5f)*.5f)/scale,
|
||||
(py-y+(sy-.5f)*.5f)/scale,0x123456);
|
||||
r+=(c>>16)&255;g+=(c>>8)&255;b+=c&255;
|
||||
}
|
||||
assert(ds[py*w+px]==((r/4)<<16|(g/4)<<8|(b/4)));
|
||||
}
|
||||
// Preserve alpha and never paint outside the lower screen.
|
||||
layout={3*scale,4*scale,w-3*scale,h-4*scale};
|
||||
az.assign(w*h,0xff123456);
|
||||
cursor.Render(w,h,x,y,8*scale,layout,az.data());
|
||||
for(int py=0;py<h;++py) for(int px=0;px<w;++px) {
|
||||
assert((az[py*w+px]>>24)==255);
|
||||
if(!layout.IsWithinTouchscreen(px,py)) assert(az[py*w+px]==0xff123456);
|
||||
else assert((az[py*w+px]&0xffffff)==ds[py*w+px]);
|
||||
}
|
||||
layout={0,0,w,h};
|
||||
}
|
||||
}
|
||||
cursor.Render(40,50,10,20,8,layout,nullptr);
|
||||
cursor.Render(40,50,10,20,8,{0,0,40,50},nullptr);
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -167,17 +218,20 @@ int main() {
|
||||
#include <initializer_list>
|
||||
struct Rect {float left,top,right,bottom; float GetHeight()const{return bottom-top;}};
|
||||
int main() {
|
||||
for(float scale : {1.f,18.f}) for(float x : {0.f,160.f,319.f})
|
||||
for(float resolution : {1.f,5.f,18.f}) for(float x : {0.f,160.f,319.f})
|
||||
for(float y : {0.f,120.f,239.f}) {
|
||||
struct {float width,height; Rect bottom_screen;} layout{
|
||||
400*scale,480*scale,{40*scale,240*scale,360*scale,480*scale}};
|
||||
struct {float projected_x,projected_y;} cursor{x*scale,y*scale};
|
||||
400*resolution,480*resolution,{40*resolution,240*resolution,360*resolution,480*resolution}};
|
||||
struct {float projected_x,projected_y;} cursor{x*resolution,y*resolution};
|
||||
""" + source[start:end] + """
|
||||
assert(sizeof(vertices)/sizeof(float)==18);
|
||||
assert(vertices[0]==cx && vertices[1]==cy); // nib is the exact touch point
|
||||
for(unsigned i=0;i<18;i+=2) {
|
||||
assert(vertices[i]>=bl && vertices[i]<=br);
|
||||
assert(vertices[i+1]>=bt && vertices[i+1]<=bb);
|
||||
assert(sizeof(vertices)/sizeof(float)==24);
|
||||
for(unsigned i=0;i<24;i+=4) {
|
||||
float px=(vertices[i]+1)*buf_w/2, py=(vertices[i+1]+1)*buf_h/2;
|
||||
assert(px>=layout.bottom_screen.left-.001 && px<=layout.bottom_screen.right+.001);
|
||||
assert(py>=layout.bottom_screen.top-.001 && py<=layout.bottom_screen.bottom+.001);
|
||||
// Every clipped vertex still measures its local position from the nib.
|
||||
assert(std::abs(px-(abs_x+vertices[i+2]*scale))<.001);
|
||||
assert(std::abs(py-(abs_y+vertices[i+3]*scale))<.001);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
last_moved = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
@@ -340,49 +353,24 @@
|
||||
@@ -340,47 +353,40 @@
|
||||
return; // No framebuffer data available
|
||||
}
|
||||
|
||||
@@ -94,24 +94,39 @@
|
||||
- int pixelIndex = y * bufferWidth + x;
|
||||
- // XOR blend for visibility on any background
|
||||
- pixels[pixelIndex] ^= cursorColor;
|
||||
+ // A pen whose nib is the touch point, clipped to the bottom screen.
|
||||
+ static const char glyph[12][13] = {
|
||||
+ "## ", "#.# ", " #.# ", " #.## ",
|
||||
+ " #..# ", " #...# ", " #...# ", " #...# ",
|
||||
+ " #...# ", " #...#", " #.# ", " # "
|
||||
+ };
|
||||
+ const int size = std::max(1, static_cast<int>(renderRatio / 8));
|
||||
+ // Same supersampled white pen and dark nib/outline as DeSmuME.
|
||||
+ const float size = std::max(1.0f, renderRatio / 8.0f);
|
||||
+ const int x = static_cast<int>(projectedX), y = static_cast<int>(projectedY);
|
||||
+ auto* pixels = static_cast<uint32_t*>(framebuffer_data);
|
||||
+ for (int row = 0; row < 12 * size; ++row) {
|
||||
+ for (int col = 0; col < 12 * size; ++col) {
|
||||
+ const int x = static_cast<int>(projectedX) + col;
|
||||
+ const int y = static_cast<int>(projectedY) + row;
|
||||
+ if (glyph[row / size][col / size] == '#' && x >= 0 && y >= 0 &&
|
||||
+ x < bufferWidth && y < bufferHeight && layout.IsWithinTouchscreen(x, y)) {
|
||||
+ pixels[y * bufferWidth + x] ^= 0x00ffffff;
|
||||
+ for (int py = std::max(0, static_cast<int>(y - 21 * size));
|
||||
+ py < std::min(bufferHeight, static_cast<int>(y + 2 * size)); ++py) {
|
||||
+ for (int px = std::max(0, static_cast<int>(x - 2 * size));
|
||||
+ px < std::min(bufferWidth, static_cast<int>(x + 17 * size)); ++px) {
|
||||
+ if (!layout.IsWithinTouchscreen(px, py))
|
||||
+ continue;
|
||||
+ const uint32_t original = pixels[py * bufferWidth + px];
|
||||
+ unsigned red = 0, green = 0, blue = 0, covered = 0;
|
||||
+ for (int sy = 0; sy < 2; ++sy) {
|
||||
+ for (int sx = 0; sx < 2; ++sx) {
|
||||
+ const float dx = (px - x + (sx - 0.5f) * 0.5f) / size;
|
||||
+ const float dy = (py - y + (sy - 0.5f) * 0.5f) / size;
|
||||
+ const float along = dx * 0.6f - dy * 0.8f;
|
||||
+ const float side = dx * 0.8f + dy * 0.6f;
|
||||
+ const float radius = std::min(1.6f, along * 0.32f);
|
||||
+ const float cap = std::max(0.0f, along - 22.0f);
|
||||
+ uint32_t colour = original;
|
||||
+ if (along >= 0 && side * side + cap * cap <= radius * radius) {
|
||||
+ ++covered;
|
||||
+ colour = along > 5.0f && along < 20.5f &&
|
||||
+ std::abs(side) < radius - 0.45f ? 0xffffff : 0x202830;
|
||||
+ }
|
||||
+ red += (colour >> 16) & 255;
|
||||
+ green += (colour >> 8) & 255;
|
||||
+ blue += colour & 255;
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- // Draw horizontal line of cursor
|
||||
- for (int y = horizontalTop; y < horizontalBottom; ++y) {
|
||||
@@ -121,11 +136,12 @@
|
||||
- // XOR blend for visibility on any background
|
||||
- pixels[pixelIndex] ^= cursorColor;
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
+ if (covered)
|
||||
+ pixels[py * bufferWidth + px] = (original & 0xff000000) |
|
||||
+ ((red / 4) << 16) | ((green / 4) << 8) | (blue / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Input
|
||||
--- a/src/citra_libretro/input/mouse_tracker.h
|
||||
+++ b/src/citra_libretro/input/mouse_tracker.h
|
||||
@@ -61,6 +61,7 @@
|
||||
@@ -138,10 +154,71 @@
|
||||
float lastMouseY;
|
||||
--- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp
|
||||
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
|
||||
@@ -1087,28 +1087,15 @@
|
||||
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;
|
||||
@@ -485,26 +485,24 @@
|
||||
present_pipelines[i] = pipeline;
|
||||
}
|
||||
|
||||
- // Build cursor pipeline (simple position-only, inverted color blending)
|
||||
+ // Build the DeSmuME-style pen pipeline (position and local pen coordinates).
|
||||
{
|
||||
const vk::VertexInputBindingDescription cursor_binding = {
|
||||
.binding = 0,
|
||||
- .stride = sizeof(float) * 2,
|
||||
+ .stride = sizeof(float) * 4,
|
||||
.inputRate = vk::VertexInputRate::eVertex,
|
||||
};
|
||||
|
||||
- const vk::VertexInputAttributeDescription cursor_attribute = {
|
||||
- .location = 0,
|
||||
- .binding = 0,
|
||||
- .format = vk::Format::eR32G32Sfloat,
|
||||
- .offset = 0,
|
||||
+ const std::array cursor_attributes = {
|
||||
+ vk::VertexInputAttributeDescription{0, 0, vk::Format::eR32G32Sfloat, 0},
|
||||
+ vk::VertexInputAttributeDescription{1, 0, vk::Format::eR32G32Sfloat, sizeof(float) * 2},
|
||||
};
|
||||
|
||||
const vk::PipelineVertexInputStateCreateInfo cursor_vertex_input = {
|
||||
.vertexBindingDescriptionCount = 1,
|
||||
.pVertexBindingDescriptions = &cursor_binding,
|
||||
- .vertexAttributeDescriptionCount = 1,
|
||||
- .pVertexAttributeDescriptions = &cursor_attribute,
|
||||
+ .vertexAttributeDescriptionCount = 2,
|
||||
+ .pVertexAttributeDescriptions = cursor_attributes.data(),
|
||||
};
|
||||
|
||||
const vk::PipelineInputAssemblyStateCreateInfo cursor_input_assembly = {
|
||||
@@ -528,11 +526,11 @@
|
||||
|
||||
const vk::PipelineColorBlendAttachmentState cursor_blend_attachment = {
|
||||
.blendEnable = true,
|
||||
- .srcColorBlendFactor = vk::BlendFactor::eOneMinusDstColor,
|
||||
- .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcColor,
|
||||
+ .srcColorBlendFactor = vk::BlendFactor::eOne,
|
||||
+ .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha,
|
||||
.colorBlendOp = vk::BlendOp::eAdd,
|
||||
.srcAlphaBlendFactor = vk::BlendFactor::eOne,
|
||||
- .dstAlphaBlendFactor = vk::BlendFactor::eZero,
|
||||
+ .dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha,
|
||||
.alphaBlendOp = vk::BlendOp::eAdd,
|
||||
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
|
||||
@@ -1075,40 +1073,20 @@
|
||||
// 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);
|
||||
@@ -155,10 +232,16 @@
|
||||
-
|
||||
- // 12 vertices = 4 triangles (2 for vertical bar, 2 for horizontal bar)
|
||||
- // clang-format off
|
||||
+ // Stylus silhouette: the nib stays exactly at the touch coordinate.
|
||||
+ // Use the existing inversion pipeline so it remains visible over the game.
|
||||
+ const auto px = [&](float v) { return std::clamp(cx + v * rw / 4.0f, bl, br); };
|
||||
+ const auto py = [&](float v) { return std::clamp(cy + v * rh / 4.0f, bt, bb); };
|
||||
+ const float scale = std::max(1.0f, layout.bottom_screen.GetHeight() / 240.0f);
|
||||
+ // Clip positions and local coordinates together, so clipping never distorts the pen.
|
||||
+ const float left = std::max(abs_x - 2 * scale, float(layout.bottom_screen.left));
|
||||
+ const float top = std::max(abs_y - 21 * scale, float(layout.bottom_screen.top));
|
||||
+ const float right = std::min(abs_x + 17 * scale, float(layout.bottom_screen.right));
|
||||
+ const float bottom = std::min(abs_y + 2 * scale, float(layout.bottom_screen.bottom));
|
||||
+ const float l = left / buf_w * 2 - 1, r = right / buf_w * 2 - 1;
|
||||
+ const float t = top / buf_h * 2 - 1, b = bottom / buf_h * 2 - 1;
|
||||
+ const float u0 = (left - abs_x) / scale, u1 = (right - abs_x) / scale;
|
||||
+ const float v0 = (top - abs_y) / scale, v1 = (bottom - abs_y) / scale;
|
||||
const float vertices[] = {
|
||||
- // Vertical bar
|
||||
- vl, vt, vr, vt, vr, vb,
|
||||
@@ -168,19 +251,63 @@
|
||||
- hl, ht, hr, hb, hl, hb,
|
||||
- };
|
||||
- // clang-format on
|
||||
+ px(0), py(0), px(1), py(4), px(4), py(1), // nib
|
||||
+ px(2), py(5), px(5), py(2), px(12), py(9), // shaft
|
||||
+ px(2), py(5), px(12), py(9), px(9), py(12),
|
||||
+ l, t, u0, v0, r, t, u1, v0, r, b, u1, v1,
|
||||
+ l, t, u0, v0, r, b, u1, v1, l, b, u0, v1,
|
||||
+ };
|
||||
|
||||
const u64 size = sizeof(vertices);
|
||||
auto [data, offset, invalidate] = vertex_buffer.Map(size, 16);
|
||||
@@ -1119,7 +1106,7 @@
|
||||
@@ -1118,8 +1096,8 @@
|
||||
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);
|
||||
- const u32 first_vertex = static_cast<u32>(offset) / (sizeof(float) * 2);
|
||||
- cmdbuf.draw(12, 1, first_vertex, 0);
|
||||
+ cmdbuf.draw(9, 1, first_vertex, 0);
|
||||
+ const u32 first_vertex = static_cast<u32>(offset) / (sizeof(float) * 4);
|
||||
+ cmdbuf.draw(6, 1, first_vertex, 0);
|
||||
});
|
||||
}
|
||||
|
||||
--- a/src/video_core/host_shaders/vulkan_cursor.vert
|
||||
+++ b/src/video_core/host_shaders/vulkan_cursor.vert
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
layout (location = 0) in vec2 vert_position;
|
||||
|
||||
+layout (location = 1) in vec2 vert_stylus;
|
||||
+layout (location = 0) out vec2 stylus;
|
||||
+
|
||||
void main() {
|
||||
+ stylus = vert_stylus;
|
||||
gl_Position = vec4(vert_position, 0.0, 1.0);
|
||||
}
|
||||
--- a/src/video_core/host_shaders/vulkan_cursor.frag
|
||||
+++ b/src/video_core/host_shaders/vulkan_cursor.frag
|
||||
@@ -5,8 +5,26 @@
|
||||
#version 450 core
|
||||
#extension GL_ARB_separate_shader_objects : enable
|
||||
|
||||
+layout (location = 0) in vec2 stylus;
|
||||
layout (location = 0) out vec4 color;
|
||||
|
||||
void main() {
|
||||
- color = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
+ // Match DeSmuME's four subpixel samples, including its integer pixel origin.
|
||||
+ vec2 pixel = fwidth(stylus);
|
||||
+ vec2 origin = stylus - 0.5 * pixel;
|
||||
+ color = vec4(0.0);
|
||||
+ for (int sy = 0; sy < 2; ++sy) {
|
||||
+ for (int sx = 0; sx < 2; ++sx) {
|
||||
+ vec2 p = origin + (vec2(sx, sy) - 0.5) * 0.5 * pixel;
|
||||
+ float along = p.x * 0.6 - p.y * 0.8;
|
||||
+ float side = p.x * 0.8 + p.y * 0.6;
|
||||
+ float radius = min(1.6, along * 0.32);
|
||||
+ float cap = max(0.0, along - 22.0);
|
||||
+ if (along >= 0.0 && side * side + cap * cap <= radius * radius) {
|
||||
+ vec3 ink = along > 5.0 && along < 20.5 && abs(side) < radius - 0.45
|
||||
+ ? vec3(1.0) : vec3(32.0, 40.0, 48.0) / 255.0;
|
||||
+ color += vec4(ink, 1.0) * 0.25;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
Reference in new issue
Block a user