mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-14 12:56:09 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6705577c9 | |||
| 732b7eb560 | |||
| 93eecca894 | |||
| d5aa10d572 | |||
| 308a995275 | |||
| f8145152c7 | |||
| c9c136bea7 | |||
| 6f9d025ad2 | |||
| ab129cf314 | |||
| 72b3f3a8a5 | |||
| 5fb3ae487c | |||
| 86e02aceba | |||
| 50ec0892c6 | |||
| f4e7fc91ba | |||
| 45c9f9bbb3 | |||
| 8e373eb714 | |||
| 75ebfaa090 | |||
| e3035ae8f2 | |||
| bcd22d9f8b | |||
| 4e7c036c7e | |||
| 19e2dba35a | |||
| 0634b4a278 | |||
| ee428deb1e | |||
| 07bc77c7e7 | |||
| 72973fe582 | |||
| c263b6af6f | |||
| e46576b4c3 | |||
| 89bcb73d47 | |||
| 7d81a724ef | |||
| 634686d59b | |||
| d0af14632e | |||
| c87986b276 | |||
| b600b90d53 | |||
| 2b979024cb | |||
| 5f676a6a55 | |||
| 2ab5b37137 | |||
| f6547fac8c | |||
| 9c685c1449 | |||
| aaf1a597ef | |||
| 850fc372b7 | |||
| bad9d8043b | |||
| 8e8e66f880 | |||
| fec5ac2298 |
+2
-2
@@ -641,7 +641,7 @@ if (ENABLE_QT)
|
||||
list(APPEND CMAKE_PREFIX_PATH "${Qt6_DIR}")
|
||||
endif()
|
||||
|
||||
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Concurrent)
|
||||
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Charts Concurrent)
|
||||
|
||||
if (YUZU_USE_QT_MULTIMEDIA)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Multimedia)
|
||||
@@ -680,7 +680,7 @@ if (ENABLE_QT)
|
||||
## Components ##
|
||||
|
||||
# Best practice is to ask for all components at once, so they are from the same version
|
||||
set(YUZU_QT_COMPONENTS Core Widgets Concurrent)
|
||||
set(YUZU_QT_COMPONENTS Core Widgets Charts Concurrent)
|
||||
if (PLATFORM_LINUX)
|
||||
list(APPEND YUZU_QT_COMPONENTS DBus)
|
||||
endif()
|
||||
|
||||
@@ -84,7 +84,11 @@ function(SystemPackageViable JSON_NAME)
|
||||
parse_object(${object})
|
||||
|
||||
string(REPLACE " " ";" find_args "${find_args}")
|
||||
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
|
||||
if (${package}_FORCE_BUNDLED)
|
||||
set(${package}_FOUND OFF)
|
||||
else()
|
||||
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
|
||||
endif()
|
||||
|
||||
set(${pkg}_VIABLE ${${package}_FOUND} PARENT_SCOPE)
|
||||
set(${pkg}_PACKAGE ${package} PARENT_SCOPE)
|
||||
|
||||
@@ -36,14 +36,17 @@ set(GIT_DESC ${BUILD_VERSION})
|
||||
# Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well
|
||||
|
||||
# Auto-updater metadata! Must somewhat mirror GitHub API endpoint
|
||||
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
|
||||
set(BUILD_AUTO_UPDATE_API "http://api.github.com")
|
||||
|
||||
if (NIGHTLY_BUILD)
|
||||
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
|
||||
set(BUILD_AUTO_UPDATE_API "api.github.com")
|
||||
set(BUILD_AUTO_UPDATE_API_PATH "/repos/")
|
||||
set(BUILD_AUTO_UPDATE_REPO "Eden-CI/Nightly")
|
||||
set(REPO_NAME "Eden Nightly")
|
||||
else()
|
||||
set(BUILD_AUTO_UPDATE_REPO "eden-emulator/Releases")
|
||||
set(BUILD_AUTO_UPDATE_WEBSITE "https://git.eden-emu.dev")
|
||||
set(BUILD_AUTO_UPDATE_API "git.eden-emu.dev")
|
||||
set(BUILD_AUTO_UPDATE_API_PATH "/api/v1/repos/")
|
||||
set(BUILD_AUTO_UPDATE_REPO "eden-emu/eden")
|
||||
set(REPO_NAME "Eden")
|
||||
endif()
|
||||
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
fill="none"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg7"
|
||||
sodipodi:docname="EdenLogoLoveWhiteV3.svg"
|
||||
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||
xml:space="preserve"
|
||||
inkscape:export-filename="dev.eden_emu.eden.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Madeline_Dev</dc:title>
|
||||
<dc:identifier>mailto:madelvidel@gmail.com</dc:identifier>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:date>2025</dc:date>
|
||||
<dc:license
|
||||
rdf:resource="https://www.gnu.org/licenses/gpl-3.0.html" />
|
||||
<dc:rights>2025 Eden Emulator Project</dc:rights>
|
||||
<dc:source>https://git.eden-emu.dev</dc:source>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs7"><linearGradient
|
||||
id="swatch14"
|
||||
inkscape:swatch="solid"><stop
|
||||
style="stop-color:#66003b;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop14" /></linearGradient><linearGradient
|
||||
id="linearGradient1"
|
||||
inkscape:collect="always"><stop
|
||||
style="stop-color:#ffffff;stop-opacity:0.50980395;"
|
||||
offset="0"
|
||||
id="stop1" /><stop
|
||||
style="stop-color:#000000;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop2" /></linearGradient><linearGradient
|
||||
id="linearGradient11"
|
||||
inkscape:collect="always"><stop
|
||||
style="stop-color:#9523a7;stop-opacity:0.50980395;"
|
||||
offset="0"
|
||||
id="stop11" /><stop
|
||||
style="stop-color:#ffabad;stop-opacity:1;"
|
||||
offset="0.99898213"
|
||||
id="stop20" /><stop
|
||||
style="stop-color:#ffffff;stop-opacity:0;"
|
||||
offset="0.99898213"
|
||||
id="stop12" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient11"
|
||||
id="linearGradient12"
|
||||
x1="109.74531"
|
||||
y1="106.54533"
|
||||
x2="431.05463"
|
||||
y2="427.85461"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
spreadMethod="reflect"
|
||||
gradientTransform="matrix(1.0945321,0,0,1.0945321,-39.661525,-35.159057)" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1"
|
||||
id="linearGradient2"
|
||||
x1="125.40197"
|
||||
y1="271.834"
|
||||
x2="431.02424"
|
||||
y2="271.834"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0964243,0,0,1.0253208,-40.187969,-20.060025)" /><filter
|
||||
inkscape:label="Light Contour"
|
||||
inkscape:menu="Image Paint and Draw"
|
||||
inkscape:menu-tooltip="Uses vertical specular light to draw lines"
|
||||
style="color-interpolation-filters:sRGB"
|
||||
id="filter11"
|
||||
x="-0.01907517"
|
||||
y="-0.054959154"
|
||||
width="1.0379885"
|
||||
height="1.1092314"><feGaussianBlur
|
||||
in="SourceGraphic"
|
||||
stdDeviation="0.38250006"
|
||||
result="result3"
|
||||
id="feGaussianBlur9" /><feComponentTransfer
|
||||
result="result1"
|
||||
in="result3"
|
||||
id="feComponentTransfer9"><feFuncR
|
||||
type="discrete"
|
||||
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
|
||||
id="feFuncR9" /><feFuncG
|
||||
type="discrete"
|
||||
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
|
||||
id="feFuncG9" /><feFuncB
|
||||
type="discrete"
|
||||
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
|
||||
id="feFuncB9" /></feComponentTransfer><feGaussianBlur
|
||||
result="result5"
|
||||
stdDeviation="0.01"
|
||||
id="feGaussianBlur10" /><feBlend
|
||||
in2="result5"
|
||||
result="result6"
|
||||
mode="lighten"
|
||||
in="result5"
|
||||
id="feBlend10" /><feColorMatrix
|
||||
in="result6"
|
||||
type="luminanceToAlpha"
|
||||
result="result2"
|
||||
id="feColorMatrix10" /><feSpecularLighting
|
||||
surfaceScale="5"
|
||||
result="result9"
|
||||
specularExponent="20"
|
||||
in="result2"
|
||||
specularConstant="1"
|
||||
id="feSpecularLighting10"><feDistantLight
|
||||
azimuth="180"
|
||||
elevation="90"
|
||||
id="feDistantLight10" /></feSpecularLighting><feComposite
|
||||
in2="result6"
|
||||
operator="arithmetic"
|
||||
in="result9"
|
||||
k1="0.4"
|
||||
k3="0.7"
|
||||
result="result3"
|
||||
id="feComposite10"
|
||||
k2="0"
|
||||
k4="0" /><feBlend
|
||||
in2="result1"
|
||||
in="result3"
|
||||
mode="normal"
|
||||
result="result8"
|
||||
id="feBlend11" /><feComposite
|
||||
in2="SourceGraphic"
|
||||
in="result8"
|
||||
operator="in"
|
||||
result="result7"
|
||||
id="feComposite11" /></filter></defs><sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.35"
|
||||
inkscape:cx="254.81481"
|
||||
inkscape:cy="260.37037"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1001"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg7"
|
||||
showguides="false" /><circle
|
||||
style="fill:url(#linearGradient12);fill-opacity:1;stroke:#e4e4e4;stroke-width:14.0448;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="path8"
|
||||
cx="256.2999"
|
||||
cy="257.2999"
|
||||
r="248.67769" /><path
|
||||
id="path15"
|
||||
style="fill:url(#linearGradient2);fill-opacity:1;stroke:#ffffff;stroke-width:16.9642;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.52156866;paint-order:stroke fill markers"
|
||||
d="m 306.72111,24.233031 c 0,0 -11.98397,40.08696 -18.0546,60.431848 -12.96613,9.503601 -21.49377,18.397701 -21.49377,18.397701 0,0 -23.41313,-31.029398 -45.74145,-43.934598 -22.32833,-12.905201 -52.42065,-11.242483 -52.42065,-11.242483 0,0 -12.50052,4e-4 -27.63117,5.537132 -15.13066,5.536732 -21.27107,9.227888 -21.27107,9.227888 0,0 15.35165,-0.410529 37.93799,6.766716 22.58635,7.177243 32.45374,11.484796 32.45374,11.484796 l 31.02752,-6.562453 -8.44161,11.074275 c 0,0 14.80259,8.920284 22.80648,16.917787 8.0039,7.9975 11.73088,12.50812 11.73088,12.50812 0,0 -13.92373,-5.43341 -56.68427,-1.74226 -42.76055,3.69116 -84.86368,56.39265 -84.86368,56.39265 0,0 41.22428,-15.9958 65.5649,-21.32747 24.34062,-5.33166 47.58524,-7.9983 47.58524,-7.9983 0,0 -18.41865,7.3827 -38.15428,38.3474 -19.73564,30.96468 -14.0351,80.18128 -14.0351,80.18128 0,0 31.35774,-59.05848 61.39977,-78.94969 30.04203,-19.89124 36.182,-20.50642 36.182,-20.50642 0,0 -24.1209,48.39514 -33.55015,126.11445 -9.42924,77.71931 30.26089,207.72959 30.26089,207.72959 l 47.80367,-6.97298 c 0,0 -45.61041,-42.24281 -49.55753,-151.33695 -3.94713,-109.09413 16.66479,-174.30453 16.66479,-174.30453 0,0 17.10508,1.23039 42.54213,32.81027 25.43704,31.57987 40.34713,57.41795 40.34713,57.41795 0,0 9.65024,-51.26683 -11.4011,-74.64415 -21.05135,-23.3773 -53.50637,-33.62931 -53.50637,-33.62931 0,0 13.70573,-6.66419 42.10313,-4.71609 28.39738,1.94813 87.30278,27.12897 87.30278,27.12896 0,0 -15.8158,-25.5214 -52.87463,-43.21167 -37.05881,-17.69028 -81.35597,0.29238 -81.35597,0.29238 0,0 4.13589,-15.37277 29.10021,-32.628037 24.96432,-17.25527 56.907,-34.36427 56.907,-34.36427 0,0 -26.3595,1.160335 -55.20025,12.035504 -7.176,2.7059 -14.064,6.299542 -20.36309,10.173104 l 17.40574,-51.051762 z" /><path
|
||||
id="path4-2-6"
|
||||
style="fill:#ffffff;fill-opacity:0.509804;stroke:#ffffff;stroke-width:8.49614;stroke-dasharray:none;stroke-opacity:0.521569"
|
||||
d="m 105.64857,363.95957 a 27.851731,25.309928 0 0 0 -7.115581,35.20471 27.851731,25.309928 0 0 0 5.656921,5.48617 c 7.16052,5.83605 21.58543,14.65841 45.88391,17.847 37.57172,4.93039 41.7105,10.38005 41.71122,10.381 -4.5e-4,-6e-4 -4.13529,-5.45309 4.14459,-39.11953 5.35491,-21.77281 1.40624,-37.16078 -2.05217,-45.31698 a 27.851731,25.309928 0 0 0 -3.63281,-6.75471 27.851731,25.309928 0 0 0 -38.74027,-6.4662 27.851731,25.309928 0 0 0 -7.11556,35.20472 27.851731,25.309928 0 0 0 -38.74025,-6.46618 z" /><path
|
||||
id="path4-2-6-1"
|
||||
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:6.89965;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 351.29524,378.32915 a 20.862787,22.283365 87.929759 0 0 -28.94663,11.7882 20.862787,22.283365 87.929759 0 0 -1.50914,6.04125 c -1.09835,7.13663 -1.16134,20.16231 7.28339,36.72931 13.0577,25.61689 10.90177,30.69793 10.90138,30.69882 2.4e-4,-5.6e-4 2.16,-5.08045 30.49632,-15.43688 18.32576,-6.69765 27.84648,-16.23659 32.287,-22.14109 a 20.862787,22.283365 87.929759 0 0 3.33646,-5.36755 20.862787,22.283365 87.929759 0 0 -12.45108,-27.05014 20.862787,22.283365 87.929759 0 0 -28.94664,11.78822 20.862787,22.283365 87.929759 0 0 -12.45106,-27.05014 z" /><path
|
||||
id="path4-2-6-1-5"
|
||||
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:6.89965;stroke-dasharray:none;stroke-opacity:0.52156866"
|
||||
d="m 83.024352,249.4105 a 22.283365,20.862787 0 0 0 -29.353581,10.73482 22.283365,20.862787 0 0 0 -1.72639,5.98278 c -1.35544,7.0923 -1.88894,20.10721 5.951801,36.96846 12.12378,26.07187 9.7857,31.07171 9.78528,31.07259 2.6e-4,-5.5e-4 2.34212,-4.99911 31.03406,-14.32513 18.555748,-6.03127 28.414858,-15.22005 33.065768,-20.96029 a 22.283365,20.862787 0 0 0 3.52818,-5.24352 22.283365,20.862787 0 0 0 -11.46577,-27.48227 22.283365,20.862787 0 0 0 -29.353588,10.73483 22.283365,20.862787 0 0 0 -11.46576,-27.48227 z" /><path
|
||||
id="path4-2-6-1-9"
|
||||
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:5.01078;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 320.97602,291.36362 a 16.625347,14.748181 0 0 0 -21.90036,7.58859 16.625347,14.748181 0 0 0 -1.28804,4.2293 c -1.01128,5.01364 -1.40932,14.21406 4.44057,26.1335 9.0454,18.43055 7.30098,21.965 7.30067,21.96562 2e-4,-3.9e-4 1.74743,-3.53394 23.15415,-10.12663 13.84423,-4.26358 21.19999,-10.75925 24.66998,-14.8171 a 16.625347,14.748181 0 0 0 2.63233,-3.70671 16.625347,14.748181 0 0 0 -8.55447,-19.42758 16.625347,14.748181 0 0 0 -21.90037,7.58859 16.625347,14.748181 0 0 0 -8.55446,-19.42758 z" /><path
|
||||
id="path4-2-6-1-9-8"
|
||||
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:4.22313;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 416.90426,193.13811 a 12.322476,14.134158 82.30909 0 0 -17.56469,8.83612 12.322476,14.134158 82.30909 0 0 -0.59553,3.65007 c -0.27175,4.26676 0.45709,11.92642 6.76182,21.10665 9.74867,14.19503 8.68847,17.32338 8.68827,17.3239 1.2e-4,-3.3e-4 1.06282,-3.12827 18.32699,-11.08266 11.16516,-5.14424 16.60821,-11.37802 19.06101,-15.14093 a 12.322476,14.134158 82.30909 0 0 1.788,-3.37453 12.322476,14.134158 82.30909 0 0 -9.45059,-15.07737 12.322476,14.134158 82.30909 0 0 -17.5647,8.83612 12.322476,14.134158 82.30909 0 0 -9.45058,-15.07737 z" /><path
|
||||
id="path4-2-6-1-9-8-4"
|
||||
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:3.48342;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 96.169666,99.754795 a 10.296362,11.508677 62.365085 0 0 -7.330189,13.841365 10.296362,11.508677 62.365085 0 0 1.473186,2.6805 c 2.008889,2.93989 6.437543,7.66269 15.415927,10.84633 13.88288,4.92276 14.77391,7.47086 14.77407,7.4713 -1e-4,-2.8e-4 -0.88925,-2.54921 6.69094,-15.67164 4.90232,-8.48655 5.37758,-15.08279 5.10162,-18.6847 a 10.296362,11.508677 62.365085 0 0 -0.5257,-3.042454 10.296362,11.508677 62.365085 0 0 -14.1348,-5.641051 10.296362,11.508677 62.365085 0 0 -7.33021,13.841385 10.296362,11.508677 62.365085 0 0 -14.134844,-5.641035 z" /><path
|
||||
id="path4-2-6-1-9-8-4-9-2"
|
||||
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:3.05139;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 296.64657,233.26347 a 8.8114666,10.319195 80.445391 0 0 -11.58972,8.06989 8.8114666,10.319195 80.445391 0 0 0.0366,2.62543 c 0.35019,3.02392 1.86017,8.32306 7.60386,14.09672 8.8812,8.92759 8.51432,11.23624 8.51426,11.23663 5e-5,-2.5e-4 0.36876,-2.30885 11.85385,-9.72788 7.4277,-4.79799 10.57103,-9.75243 11.86523,-12.65455 a 8.8114666,10.319195 80.445391 0 0 0.86244,-2.55882 8.8114666,10.319195 80.445391 0 0 -8.77837,-9.57865 8.8114666,10.319195 80.445391 0 0 -11.58975,8.06989 8.8114666,10.319195 80.445391 0 0 -8.77842,-9.57866 z" /><path
|
||||
id="path4-2-6-1-9-8-4-9-2-6"
|
||||
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:2.49738;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 403.95446,91.44744 a 7.0950375,8.5844443 82.142743 0 0 -9.64888,6.492875 7.0950375,8.5844443 82.142743 0 0 0.0305,2.112375 c 0.29155,2.43298 1.54866,6.69657 6.33049,11.34195 7.39392,7.18298 7.08848,9.04046 7.08843,9.04078 4e-5,-2.1e-4 0.307,-1.85765 9.86876,-7.82686 6.18383,-3.86038 8.80077,-7.84663 9.87824,-10.18161 a 7.0950375,8.5844443 82.142743 0 0 0.718,-2.05879 7.0950375,8.5844443 82.142743 0 0 -7.30831,-7.706798 7.0950375,8.5844443 82.142743 0 0 -9.64889,6.492875 7.0950375,8.5844443 82.142743 0 0 -7.30834,-7.706797 z" /><path
|
||||
id="path4-2-6-1-9-8-4-9-5"
|
||||
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:7.54176;stroke-dasharray:none;stroke-opacity:0.521569"
|
||||
d="m 410.46352,265.86621 a 24.802737,22.39469 2.0523303 0 0 -30.17074,15.95063 24.802737,22.39469 2.0523303 0 0 -0.74427,6.61415 c -0.12337,7.7333 1.79614,21.62064 13.65297,38.28529 18.33368,25.76802 16.72835,31.4347 16.72811,31.43572 1.4e-4,-6.3e-4 1.60998,-5.66652 31.32518,-20.02007 19.21759,-9.28266 28.27643,-20.56192 32.27921,-27.37361 a 24.802737,22.39469 2.0523303 0 0 2.8654,-6.11021 24.802737,22.39469 2.0523303 0 0 -17.88248,-27.36631 24.802737,22.39469 2.0523303 0 0 -30.17081,15.95066 24.802737,22.39469 2.0523303 0 0 -17.88257,-27.36625 z" /><path
|
||||
id="path4-2-6-1-9-8-5"
|
||||
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:3.6405;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 114.43099,189.33754 a 10.333135,12.525309 79.004057 0 0 -14.00848,9.75499 10.333135,12.525309 79.004057 0 0 0.0547,3.08382 c 0.43602,3.54457 2.28588,9.73496 9.26636,16.38295 10.79359,10.27954 10.35819,13.00063 10.3581,13.00111 6e-5,-3e-4 0.4377,-2.72141 14.32197,-11.70923 8.9793,-5.8126 12.76786,-11.70785 14.32433,-15.14812 a 10.333135,12.525309 79.004057 0 0 1.03481,-3.02677 10.333135,12.525309 79.004057 0 0 -10.67161,-11.04688 10.333135,12.525309 79.004057 0 0 -14.00848,9.75501 10.333135,12.525309 79.004057 0 0 -10.6716,-11.04688 z" /><path
|
||||
id="path4-2-6-1-9-8-4-9-2-2"
|
||||
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:3.62674;stroke-dasharray:none;stroke-opacity:0.519962"
|
||||
d="m 178.56055,273.32338 a 10.656883,12.053184 66.089768 0 0 -7.97821,14.16205 10.656883,12.053184 66.089768 0 0 1.49012,2.79504 c 2.04675,3.07265 6.5954,8.03049 15.94965,11.48124 14.46412,5.33577 15.34612,7.98353 15.34628,7.98398 -8e-5,-2.8e-4 -0.88012,-2.64887 7.3455,-16.0639 5.31973,-8.67576 5.95524,-15.47956 5.74045,-19.20452 a 10.656883,12.053184 66.089768 0 0 -0.48843,-3.15169 10.656883,12.053184 66.089768 0 0 -14.7135,-6.08217 10.656883,12.053184 66.089768 0 0 -7.97826,14.1621 10.656883,12.053184 66.089768 0 0 -14.71355,-6.08215 z" /></svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
+1
@@ -0,0 +1 @@
|
||||
#e48cc9ff
|
||||
+225
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
+1
-4
@@ -6,7 +6,4 @@ Debugging on physical hardware can get tedious and time consuming. Users are emp
|
||||
|
||||
**Standard key prefix**: Allows to redirect the key manager to a file other than `prod.keys` (for example `other` would redirect to `other.keys`). This is useful for testing multiple keysets. Default is `prod`.
|
||||
|
||||
**Changing serial**: Very basic way to set debug values for the serial (and battery number). Developers do not need to write the full serial as it will be writen in-place (that is, it will be filled with the default serial and then overwrite the serial from the beginning).
|
||||
- Battery serial: `YUZU0EMULATOR14022024`
|
||||
- Board serial: `YUZ10000000001`
|
||||
If the user were to set their board serial as `ABC`, then it will be written in-place and the resulting serial would be `ABC10000000001`. There are no underlying checks to ensure correctness of serials other than a hard limit of 16-characters for both.
|
||||
**Changing serial**: Very basic way to set debug values for the serial (and battery number). Developers do not need to write the full serial as only the first digits (excluding the last) will be accoutned for. Region settings will affect the generated serial. The serial corresponds to a non-OLED/Lite console.
|
||||
|
||||
Vendored
+2
-8
@@ -27,11 +27,7 @@ set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
|
||||
|
||||
# Xbyak
|
||||
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
|
||||
if (PLATFORM_SUN OR PLATFORM_OPENBSD OR PLATFORM_NETBSD OR PLATFORM_DRAGONFLY)
|
||||
AddJsonPackage(xbyak_sun)
|
||||
else()
|
||||
AddJsonPackage(xbyak)
|
||||
endif()
|
||||
AddJsonPackage(xbyak)
|
||||
endif()
|
||||
|
||||
# enet
|
||||
@@ -411,12 +407,10 @@ if (APPLE)
|
||||
# moltenvk
|
||||
if (NOT YUZU_USE_BUNDLED_MOLTENVK)
|
||||
find_library(MOLTENVK_LIBRARY MoltenVK)
|
||||
else()
|
||||
unset(MOLTENVK_LIBRARY)
|
||||
endif()
|
||||
|
||||
# TODO: kosmickrisp?
|
||||
if (NOT MOLTENVK_LIBRARY)
|
||||
if (NOT MOLTENVK_LIBRARY OR YUZU_USE_BUNDLED_MOLTENVK)
|
||||
AddJsonPackage(moltenvk)
|
||||
|
||||
set(MOLTENVK_LIBRARY "${moltenvk_SOURCE_DIR}/MoltenVK/dylib/macOS/libMoltenVK.dylib" CACHE STRING "" FORCE)
|
||||
|
||||
Vendored
+3
-16
@@ -48,23 +48,12 @@
|
||||
"0001-fix-missing-decl.patch"
|
||||
]
|
||||
},
|
||||
"xbyak_sun": {
|
||||
"package": "xbyak",
|
||||
"repo": "herumi/xbyak",
|
||||
"tag": "v%VERSION%",
|
||||
"hash": "b40dade90fb0e46a2bd52934f7ce461e37be931b571e58cbe2203bc08ed5b54c7ff1a29026c74c7f9805e4e3f6c9636deca528e6b4a8093ce7eae145218599f1",
|
||||
"git_version": "7.29",
|
||||
"bundled": true,
|
||||
"skip_updates": true
|
||||
},
|
||||
"xbyak": {
|
||||
"package": "xbyak",
|
||||
"repo": "herumi/xbyak",
|
||||
"tag": "v%VERSION%",
|
||||
"hash": "1042090405c426e339506c179d53e91d4d545ce9c9f53d8f797caa092d589f913a9bcb9c8f31c4c60870acb954c556e305fb6732c66bc3c8f1cd924f9172def9",
|
||||
"git_version": "7.22",
|
||||
"bundled": true,
|
||||
"skip_updates": true
|
||||
"hash": "ac333d7bea1d61865bebebb116201a58db431946aa2f11aa042ef5795c390ff30af4d6c90ed3b3d24443a1d430703b08f14fc13b2fa405c155a241456ed78a47",
|
||||
"git_version": "7.33.2"
|
||||
},
|
||||
"oaknut": {
|
||||
"repo": "eden-emulator/oaknut",
|
||||
@@ -161,7 +150,7 @@
|
||||
"package": "SDL2",
|
||||
"name": "SDL2",
|
||||
"repo": "crueter-ci/SDL2",
|
||||
"version": "2.32.10-a65111bd2d",
|
||||
"version": "2.32.10-cf5dabd6ea",
|
||||
"min_version": "2.26.4"
|
||||
},
|
||||
"catch2": {
|
||||
@@ -195,7 +184,6 @@
|
||||
"repo": "libsdl-org/SDL",
|
||||
"tag": "release-%VERSION%",
|
||||
"hash": "d5622d6bb7266f7942a7b8ad43e8a22524893bf0c2ea1af91204838d9b78d32768843f6faa248757427b8404b8c6443776d4afa6b672cd8571a4e0c03a829383",
|
||||
"key": "generic",
|
||||
"bundled": true,
|
||||
"git_version": "2.32.10",
|
||||
"skip_updates": true
|
||||
@@ -205,7 +193,6 @@
|
||||
"repo": "libsdl-org/SDL",
|
||||
"sha": "cc016b0046",
|
||||
"hash": "b8d9873446cdb922387471df9968e078714683046674ef0d0edddf8e25da65a539a3bae83d635496b970237f90b07b36a69f8d7855d450de59311d6d6e8c3dbc",
|
||||
"key": "steamdeck",
|
||||
"bundled": true,
|
||||
"skip_updates": "true"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// import android.annotation.SuppressLint
|
||||
import com.android.build.gradle.api.ApplicationVariant
|
||||
import kotlin.collections.setOf
|
||||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||||
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
|
||||
import org.gradle.api.tasks.Copy
|
||||
@@ -13,8 +15,7 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-parcelize")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
kotlin("plugin.serialization") version "2.3.0"
|
||||
kotlin("plugin.serialization") version "1.9.20"
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
|
||||
id("com.github.triplet.play") version "3.8.6"
|
||||
@@ -42,7 +43,6 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -50,10 +50,8 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
packaging {
|
||||
@@ -67,7 +65,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "dev.eden.eden_emulator"
|
||||
minSdk = 26
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionName = getGitVersion()
|
||||
versionCode = autoVersion
|
||||
@@ -269,10 +267,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
// Inclusion to exclude build/ dir from non-Android
|
||||
@@ -298,7 +292,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
|
||||
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
||||
|
||||
ktlint {
|
||||
version.set("0.50.0")
|
||||
version.set("0.47.1")
|
||||
android.set(true)
|
||||
ignoreFailures.set(false)
|
||||
disabledRules.set(
|
||||
@@ -323,42 +317,29 @@ play {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.compose:compose-bom:2026.01.01")
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3:1.4.0")
|
||||
implementation("androidx.activity:activity-compose:1.12.3")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.7")
|
||||
implementation("io.coil-kt:coil-compose:2.7.0")
|
||||
implementation("io.coil-kt:coil-svg:2.7.0")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
||||
implementation("androidx.documentfile:documentfile:1.1.0")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.6")
|
||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:5.3.2")
|
||||
implementation("io.coil-kt:coil:2.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.2.0")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.21.0")
|
||||
implementation("androidx.window:window:1.5.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0")
|
||||
implementation("org.commonmark:commonmark:0.27.1")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.9.7")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.9.7")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("io.coil-kt:coil:2.2.2")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
|
||||
implementation("androidx.window:window:1.3.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("org.commonmark:commonmark:0.22.0")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
|
||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
|
||||
implementation("androidx.compose.ui:ui-graphics-android:1.10.2")
|
||||
implementation("androidx.compose.ui:ui-text-android:1.10.2")
|
||||
implementation("net.swiftzer.semver:semver:2.1.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("androidx.compose.ui:ui-graphics-android:1.7.8")
|
||||
implementation("androidx.compose.ui:ui-text-android:1.7.8")
|
||||
implementation("net.swiftzer.semver:semver:2.0.0")
|
||||
}
|
||||
|
||||
fun runGitCommand(command: List<String>): String {
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
fill="none"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg7"
|
||||
sodipodi:docname="base.svg.2026_01_12_14_43_47.0.svg"
|
||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||
inkscape:export-filename="base.svg.2026_01_12_14_43_47.0.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs7">
|
||||
<linearGradient
|
||||
id="linearGradient1"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#ff2e88;stop-opacity:0.5;"
|
||||
offset="0"
|
||||
id="stop3" />
|
||||
<stop
|
||||
style="stop-color:#bf42f6;stop-opacity:0.5;"
|
||||
offset="0.44631511"
|
||||
id="stop4" />
|
||||
<stop
|
||||
style="stop-color:#5da5ed;stop-opacity:0.5;"
|
||||
offset="0.90088946"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient138"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#ff2e88;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop152" />
|
||||
<stop
|
||||
style="stop-color:#bf42f6;stop-opacity:1;"
|
||||
offset="0.44971901"
|
||||
id="stop137" />
|
||||
<stop
|
||||
style="stop-color:#5da5ed;stop-opacity:1;"
|
||||
offset="0.89793283"
|
||||
id="stop138" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch37"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop37" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch28"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#252525;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop28" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch27"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop27" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch15"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop16" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient14"
|
||||
inkscape:swatch="gradient">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop14" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop15" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch9"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop10" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swatch8"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop9" />
|
||||
</linearGradient>
|
||||
<rect
|
||||
x="22.627417"
|
||||
y="402.76802"
|
||||
width="521.34025"
|
||||
height="248.94868"
|
||||
id="rect24" />
|
||||
<linearGradient
|
||||
id="linearGradient11"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
style="stop-color:#ff2e88;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop11" />
|
||||
<stop
|
||||
style="stop-color:#bf42f6;stop-opacity:1;"
|
||||
offset="0.44971901"
|
||||
id="stop154" />
|
||||
<stop
|
||||
style="stop-color:#5da5ed;stop-opacity:1;"
|
||||
offset="0.89793283"
|
||||
id="stop12" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient138"
|
||||
id="linearGradient6"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.118028,0,0,1.116699,-46.314723,-42.388667)"
|
||||
x1="270.39996"
|
||||
y1="40.000019"
|
||||
x2="270.39996"
|
||||
y2="494.39996"
|
||||
spreadMethod="pad" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath18">
|
||||
<circle
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
|
||||
id="circle18"
|
||||
cx="-246.8315"
|
||||
cy="246.8338"
|
||||
inkscape:label="Circle"
|
||||
r="191.89999" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath22">
|
||||
<circle
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
|
||||
id="circle22"
|
||||
cx="256"
|
||||
cy="256"
|
||||
inkscape:label="Circle"
|
||||
r="191.89999" />
|
||||
</clipPath>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient11"
|
||||
id="linearGradient27"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-6.9401139e-5,-2.8678628)"
|
||||
x1="256.00012"
|
||||
y1="102.94693"
|
||||
x2="256.00012"
|
||||
y2="409.05307" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath128">
|
||||
<circle
|
||||
style="fill:none;fill-opacity:1;stroke:#03ffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle128"
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="192" />
|
||||
</clipPath>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient1"
|
||||
id="linearGradient2"
|
||||
x1="256"
|
||||
y1="64"
|
||||
x2="256"
|
||||
y2="448"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.3229974,0,0,1.3214002,-82.687336,-82.290326)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="261.62951"
|
||||
inkscape:cy="230.87036"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1008"
|
||||
inkscape:window-x="1080"
|
||||
inkscape:window-y="351"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg7" />
|
||||
<path
|
||||
id="path8-7"
|
||||
style="display:inline;mix-blend-mode:multiply;fill:url(#linearGradient6);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2);stroke-width:3.9666;stroke-dasharray:none;stroke-opacity:0.566238;paint-order:stroke fill markers"
|
||||
inkscape:label="Circle"
|
||||
d="M 256,2.2792898 A 254.0155,253.71401 0 0 0 150.68475,25.115202 c 19.54414,1.070775 38.74692,5.250294 51.56848,11.647658 14.14361,7.056691 28.63804,19.185961 39.4212,29.347551 h 40.60981 c 1.03847,-0.68139 2.10297,-1.36938 3.1938,-2.05957 5.45602,-15.78533 14.79164,-43.183497 19.49612,-57.0097682 A 254.0155,253.71401 0 0 0 256,2.2792898 Z m 61.57106,7.567234 -18.26098,46.1544672 c 7.79702,-4.13918 16.35655,-7.87447 25.20671,-10.87081 23.1229,-7.828433 43.96931,-10.170904 54.94058,-10.868226 A 254.0155,253.71401 0 0 0 317.57106,9.8465238 Z m 65.39277,26.4001532 c -9.68256,4.806644 -33.05532,16.642034 -55.68217,29.863734 H 424.4677 A 254.0155,253.71401 0 0 0 382.96383,36.246677 Z M 113.90698,45.690231 A 254.0155,253.71401 0 0 0 87.532302,66.110411 H 194.2739 c -1.47402,-0.80231 -2.35141,-1.25949 -2.35141,-1.25949 l 10.4496,-11.83348 -38.40568,7.01234 c 0,1e-5 -12.21537,-4.60266 -40.17313,-12.27223 -3.45336,-0.94731 -6.75329,-1.61824 -9.8863,-2.06732 z m -36.803618,30.18635 a 254.0155,253.71401 0 0 0 -34.88372,43.090929 h 59.976738 c 18.11461,-12.04145 40.14252,-22.882149 62.31266,-24.534159 52.93006,-3.9444 70.16538,1.86342 70.16538,1.86342 0,0 -4.612,-4.8206 -14.51938,-13.36656 -2.72366,-2.34942 -6.0844,-4.77373 -9.52455,-7.05363 z m 174.472868,0 c 4.57322,4.7186 7.29716,7.83565 7.29716,7.83565 0,0 3.53501,-3.18484 9.62532,-7.83565 z m 60.27649,0 c -21.56573,15.45339 -25.4703,27.979669 -25.4703,27.979669 0,0 54.83326,-19.215729 100.70543,-0.31228 11.63986,4.79661 21.58481,10.13159 29.94832,15.42354 h 52.74419 A 254.0155,253.71401 0 0 0 434.89664,75.876581 Z M 36.250648,128.73367 A 254.0155,253.71401 0 0 0 16.372095,171.82459 H 147.45478 c 1.45695,-2.5815 3.06539,-5.08648 4.83979,-7.48982 14.23694,-19.28301 27.92088,-30.0088 36.86047,-35.6011 h -30.25323 c -5.87346,0.93472 -12.04945,1.99094 -18.28166,3.16937 -30.12936,5.69727 -81.157618,22.78945 -81.157618,22.78945 0,0 11.47125,-12.39249 29.11369,-25.95882 z m 265.630492,0 c 33.48676,11.2434 52.42799,26.78443 62.7752,43.09092 h 130.97157 a 254.0155,253.71401 0 0 0 -19.87856,-43.09092 h -44.81136 c 14.85233,11.5863 21.59948,20.9854 21.59948,20.9854 0,0 -33.5226,-12.37087 -66.0646,-20.9854 z m -45.96641,16.27007 c -1.00419,0.0106 -10.12705,0.72026 -44.98966,20.64729 -3.12132,1.78406 -6.25434,3.86182 -9.37468,6.17356 h 41.81911 c 7.17181,-17.34774 12.64083,-26.82085 12.64083,-26.82085 0,0 -0.0287,-7.1e-4 -0.0957,0 z m 14.18088,0.0465 c 0,0 -3.31228,9.32762 -7.30492,26.77438 h 51.78554 C 287.6577,146.14158 270.09561,145.0502 270.09561,145.0502 Z M 13.152456,181.59075 A 254.0155,253.71401 0 0 0 3.927651,224.68167 H 134.1447 c 0.56161,-12.72411 2.67825,-28.50188 8.61499,-43.09092 z m 176.661504,0 c -14.27121,13.10564 -27.60733,29.58761 -37.56073,43.09092 h 73.3721 c 4.47018,-16.79061 9.35068,-31.26371 13.86562,-43.09092 z m 70.85787,0 c -2.41384,11.76417 -4.9032,26.20707 -6.94831,43.09092 H 360.4832 c -8.32133,-10.88917 -20.66988,-26.17008 -36.35141,-43.09092 z m 109.17313,0 c 6.63611,15.24089 6.92441,30.5373 5.57882,43.09092 h 132.64857 a 254.0155,253.71401 0 0 0 -9.22481,-43.09092 z M 2.90181,234.44783 A 254.0155,253.71401 0 0 0 1.984498,255.9933 254.0155,253.71401 0 0 0 2.90181,277.53876 h 211.89923 c 2.25762,-15.52555 5.14325,-29.93448 8.3385,-43.09093 h -77.8863 c -6.46396,9.27617 -10.33076,15.56549 -10.33076,15.56549 0,0 -0.82623,-6.14945 -0.9354,-15.56549 z m 249.72093,0 c -1.3692,13.09684 -2.4456,27.49209 -3.02068,43.09093 h 259.49613 a 254.0155,253.71401 0 0 0 0.91731,-21.54546 254.0155,253.71401 0 0 0 -0.91731,-21.54547 H 374.02584 c -0.445,2.5469 -0.90878,4.89768 -1.32817,7.01751 0,0 -1.69726,-2.53821 -4.94056,-7.01751 z M 3.927651,287.30493 a 254.0155,253.71401 0 0 0 9.224805,43.09091 H 214.04393 c -1.29238,-15.40742 -1.57503,-30.04388 -0.41861,-43.09091 z m 245.385009,0 c -0.30355,13.54349 -0.22032,27.92598 0.36951,43.09091 h 249.16537 a 254.0155,253.71401 0 0 0 9.22481,-43.09091 z M 16.369511,340.16201 a 254.0155,253.71401 0 0 0 19.878554,43.09091 H 221.4677 c -2.69781,-14.4523 -4.96108,-29.01285 -6.4832,-43.09091 z m 233.842379,0 c 1.15864,15.47765 3.81286,29.83979 7.51679,43.09091 h 218.02325 a 254.0155,253.71401 0 0 0 19.87856,-43.09091 z M 42.217052,393.01909 a 254.0155,253.71401 0 0 0 34.88372,43.09093 H 233.09561 c -3.40902,-13.67281 -6.76794,-28.2531 -9.73902,-43.09093 z m 218.490958,0 c 5.34985,16.15926 12.22007,30.51982 19.68733,43.09093 h 154.50389 a 254.0155,253.71401 0 0 0 34.88371,-43.09093 z M 87.529722,445.87618 a 254.0155,253.71401 0 0 0 166.229968,63.8208 c -3.67805,-12.0825 -10.85464,-35.49828 -18.18088,-63.8208 z m 199.010328,0 c 17.5887,26.43772 36.99259,43.60598 47.33592,51.61309 a 254.0155,253.71401 0 0 0 90.59431,-51.61309 z" />
|
||||
<path
|
||||
id="path27"
|
||||
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient27);stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="m 318.98012,441.7375 c -9.87518,-6.73978 -64.39137,-49.0272 -67.68975,-127.81978 -3.69298,-88.21893 15.36468,-141.91029 15.36468,-141.91029 0,0 16.00378,0.99513 39.80316,26.53195 23.79939,25.53753 37.74965,46.43102 37.74965,46.43102 3.91262,-19.79992 12.84563,-66.32402 -60.72865,-87.55523 0,0 12.82326,-5.38883 39.3925,-3.81382 26.56907,1.57572 81.6822,21.93799 81.6822,21.93799 0,0 -14.79766,-20.63773 -49.47063,-34.94295 -34.67291,-14.30533 -76.1182,0.23644 -76.1182,0.23644 0,0 3.86959,-12.43127 27.22669,-26.38478 23.35718,-13.9537 49.27409,-26.501533 49.27409,-26.501533 0,0 -21.97854,-0.26548 -47.67725,8.44535 -6.68948,2.267506 -13.15863,5.094213 -19.05208,8.226563 l 16.05803,-40.634103 -4.4617,-1.89059 -5.1305,-0.95965 c 0,0 -11.24072,33.12428 -16.92051,49.576513 -12.13137,7.68489 -20.11005,14.87735 -20.11005,14.87735 0,0 -21.90573,-25.09227 -42.79668,-35.527803 -26.03412,-13.00525 -86.88249,-13.90359 -94.0044,10.401173 0,0 13.56804,-7.884703 34.70032,-2.080917 21.13214,5.803997 30.3644,9.287307 30.3644,9.287307 l 29.02989,-5.30681 -7.89811,8.95527 c 0,0 13.8496,7.21324 21.33822,13.68063 7.48859,6.46722 10.9757,10.11472 10.9757,10.11472 0,0 -13.02739,-4.39388 -53.03507,-1.40893 -40.00771,2.98473 -79.40016,45.60209 -79.40016,45.60209 0,0 38.57037,-12.93531 61.34393,-17.24677 22.77354,-4.31126 44.52166,-6.46757 44.52166,-6.46757 0,0 -17.23298,5.97003 -35.69792,31.00932 -18.46522,25.03987 -13.13146,64.83866 -13.13146,64.83866 0,0 29.33874,-47.7577 57.44675,-63.84249 28.10798,-16.08527 34.0799,-15.6238 34.0799,-15.6238 0,0 -22.56785,39.13486 -31.39017,101.98268 -8.03005,57.2039 26.77689,163.75449 31.1572,178.89699"
|
||||
sodipodi:nodetypes="cscsccscscscsccccccscscccscscscscscsc"
|
||||
inkscape:label="MainOutline"
|
||||
clip-path="url(#clipPath128)"
|
||||
transform="matrix(1.3229974,0,0,1.3214002,-82.687282,-82.278451)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,184 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.background
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.request.ImageRequest
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import kotlin.math.pow
|
||||
|
||||
@Composable
|
||||
fun RetroGridBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
lineColor: Color = EdenTheme.colors.primary.copy(alpha = 0.2f),
|
||||
spacing: Dp = 40.dp,
|
||||
lineWidth: Dp = 2.dp,
|
||||
speedDpPerSecond: Float = 12f,
|
||||
fadeColor: Color = EdenTheme.colors.background,
|
||||
logoScale: Float = 0.35f,
|
||||
logoAlpha: Float = 0.25f,
|
||||
horizonRatio: Float = 0.45f,
|
||||
logoAssetPath: String = "file:///android_asset/base.svg", // correct way?
|
||||
logoWidth: Dp? = null,
|
||||
logoHeight: Dp? = null,
|
||||
logoCropBottomRatio: Float = 0.33f,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
val durationMs = remember(spacing, speedDpPerSecond, density) {
|
||||
val spacingPx = spacing.value * density.density
|
||||
val speedPxPerSecond = speedDpPerSecond * density.density
|
||||
((spacingPx / speedPxPerSecond) * 1000f).toInt().coerceAtLeast(300)
|
||||
}
|
||||
|
||||
val transition = rememberInfiniteTransition(label = "grid")
|
||||
val phase by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "gridPhase",
|
||||
)
|
||||
|
||||
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
||||
val baseSize = maxWidth * logoScale
|
||||
val resolvedWidth = logoWidth ?: baseSize
|
||||
val resolvedHeight = logoHeight ?: baseSize
|
||||
val cropRatio = logoCropBottomRatio.coerceIn(0f, 0.9f)
|
||||
val visibleHeight = resolvedHeight * (1f - cropRatio)
|
||||
val visibleHeightPx = with(density) { visibleHeight.toPx() }
|
||||
val horizonY = maxHeight * horizonRatio
|
||||
val logoOffsetY = horizonY - (resolvedHeight * 0.5f) - 50.dp
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.drawWithCache {
|
||||
val spacingPx = spacing.toPx().coerceAtLeast(8f)
|
||||
val strokePx = lineWidth.toPx().coerceAtLeast(1f)
|
||||
val horizonScale = 0.18f
|
||||
val overscan = 5.0f
|
||||
val perspectivePower = 2.2f
|
||||
onDrawBehind {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
val centerX = width * 0.5f
|
||||
val horizonYpx = height * horizonRatio
|
||||
val depth = (height - horizonYpx).coerceAtLeast(1f)
|
||||
val offset = (1f - phase) * spacingPx
|
||||
val baseHalf = centerX * overscan
|
||||
val horizonHalf = baseHalf * horizonScale
|
||||
val lineCount = (depth / (spacingPx * 0.65f))
|
||||
.toInt()
|
||||
.coerceAtLeast(20)
|
||||
|
||||
var x = centerX - baseHalf - spacingPx * 6f
|
||||
while (x <= centerX + baseHalf + spacingPx * 6f) {
|
||||
val endX = centerX + (x - centerX) * horizonScale
|
||||
drawLine(
|
||||
color = lineColor,
|
||||
start = Offset(x, height),
|
||||
end = Offset(endX, horizonYpx),
|
||||
strokeWidth = strokePx,
|
||||
)
|
||||
x += spacingPx
|
||||
}
|
||||
|
||||
var i = 0
|
||||
while (i <= lineCount) {
|
||||
val z = ((i + (offset / spacingPx)) / lineCount.toFloat())
|
||||
.coerceIn(0f, 1f)
|
||||
val zCurve = z.toDouble().pow(perspectivePower.toDouble()).toFloat()
|
||||
val y = horizonYpx + depth * zCurve
|
||||
val halfWidth = horizonHalf + (baseHalf - horizonHalf) * z
|
||||
drawLine(
|
||||
color = lineColor,
|
||||
start = Offset(centerX - halfWidth, y),
|
||||
end = Offset(centerX + halfWidth, y),
|
||||
strokeWidth = strokePx,
|
||||
)
|
||||
i += 1
|
||||
}
|
||||
|
||||
val fadeHeight = height * 0.12f
|
||||
val solidHeight = (horizonYpx - fadeHeight).coerceAtLeast(0f)
|
||||
if (solidHeight > 0f) {
|
||||
drawRect(
|
||||
color = fadeColor,
|
||||
topLeft = Offset(0f, 0f),
|
||||
size = Size(width, solidHeight),
|
||||
)
|
||||
}
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(fadeColor, Color.Transparent),
|
||||
startY = horizonYpx - fadeHeight,
|
||||
endY = horizonYpx + fadeHeight,
|
||||
),
|
||||
topLeft = Offset(0f, horizonYpx - fadeHeight),
|
||||
size = Size(width, fadeHeight * 2f),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(logoAssetPath)
|
||||
.decoderFactory(SvgDecoder.Factory())
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.requiredWidth(resolvedWidth)
|
||||
.requiredHeight(resolvedHeight)
|
||||
.offset(y = logoOffsetY)
|
||||
.drawWithContent {
|
||||
clipRect(
|
||||
left = 0f,
|
||||
top = 0f,
|
||||
right = size.width,
|
||||
bottom = visibleHeightPx,
|
||||
) {
|
||||
this@drawWithContent.drawContent()
|
||||
}
|
||||
}
|
||||
.graphicsLayer(alpha = logoAlpha),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.MarqueeAnimationMode
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import dev.eden.emu.ui.theme.Shapes
|
||||
import dev.eden.emu.ui.utils.ConfirmKeys
|
||||
import dev.eden.emu.ui.utils.MenuKeys
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun EdenTile(
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
imageUri: String? = null,
|
||||
iconSize: Dp = 140.dp,
|
||||
tileHeight: Dp = 168.dp,
|
||||
useLargerFont: Boolean = false,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val borderColor = if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(iconSize)
|
||||
.height(tileHeight)
|
||||
.semantics { role = Role.Button }
|
||||
.onPreviewKeyEvent { e ->
|
||||
if (e.type == KeyEventType.KeyUp) {
|
||||
when {
|
||||
e.key in ConfirmKeys -> { onClick(); true }
|
||||
e.key in MenuKeys -> { onLongClick?.invoke(); true }
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
}
|
||||
.focusable(interactionSource = interactionSource)
|
||||
.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
// Game icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(iconSize)
|
||||
.background(EdenTheme.colors.surface, Shapes.medium)
|
||||
.border(BorderStroke(3.dp, borderColor), Shapes.medium)
|
||||
.clip(Shapes.medium),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (imageUri.isNullOrBlank()) {
|
||||
BasicText(
|
||||
text = title.take(1).uppercase(Locale.getDefault()),
|
||||
style = EdenTheme.typography.title.copy(color = EdenTheme.colors.onSurface),
|
||||
)
|
||||
} else {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
android.widget.ImageView(ctx).apply {
|
||||
layoutParams = android.view.ViewGroup.LayoutParams(-1, -1)
|
||||
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||
setImageResource(R.drawable.default_icon)
|
||||
GameIconUtils.loadGameIcon(Game(title, imageUri, "0", "", "", false), this)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Game title with marquee
|
||||
MarqueeText(title, isFocused, 32.dp, useLargerFont, Modifier.width(iconSize))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarqueeText(
|
||||
text: String,
|
||||
isAnimating: Boolean,
|
||||
height: Dp,
|
||||
useLargerFont: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val style = if (useLargerFont) {
|
||||
EdenTheme.typography.body.copy(Color.White, 18.sp, fontWeight = FontWeight.Bold)
|
||||
} else {
|
||||
EdenTheme.typography.label.copy(Color.White)
|
||||
}
|
||||
|
||||
Box(modifier.height(height).clipToBounds(), Alignment.Center) {
|
||||
BasicText(
|
||||
text = text,
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = if (isAnimating) {
|
||||
Modifier.basicMarquee(
|
||||
iterations = Int.MAX_VALUE,
|
||||
animationMode = MarqueeAnimationMode.Immediately,
|
||||
initialDelayMillis = 150, // does not seem to take effect
|
||||
repeatDelayMillis = 1000,
|
||||
velocity = 30.dp,
|
||||
)
|
||||
} else Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import dev.eden.emu.ui.theme.Dimens
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import dev.eden.emu.ui.utils.ConfirmKeys
|
||||
|
||||
/**
|
||||
* Expandable search bar for controller-based navigation.
|
||||
* ________________________________________________________________
|
||||
* Note: Focus Manager on this doesn't work good. Need a hide impl.
|
||||
* when it looses focus for better Controller Experience
|
||||
* ________________________________________________________________
|
||||
*/
|
||||
@Composable
|
||||
fun ExpandableSearchBar(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
isExpanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val searchHint = context.getString(R.string.home_search_games)
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val iconInteractionSource = remember { MutableInteractionSource() }
|
||||
val isIconFocused by iconInteractionSource.collectIsFocusedAsState()
|
||||
|
||||
// Track if we have an active filter (submitted search, if keyboard just hides, it kinda breaks)
|
||||
val hasActiveFilter = query.isNotEmpty() && !isExpanded
|
||||
val textFieldFocusRequester = remember { FocusRequester() }
|
||||
|
||||
// Focus text field when expanded
|
||||
LaunchedEffect(isExpanded) {
|
||||
if (isExpanded) {
|
||||
delay(100)
|
||||
try {
|
||||
textFieldFocusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a fixed width container to prevent layout shift.
|
||||
Box(
|
||||
modifier = modifier.width(if (isExpanded) 300.dp else 48.dp),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
if (isExpanded) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(300.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(EdenTheme.colors.surface)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = EdenTheme.colors.primary,
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
// Text input - NO onFocusChanged to prevent auto-close issues
|
||||
// (Needs to be "submitted", hiding keyboard breaks focus flow)
|
||||
BasicTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(textFieldFocusRequester)
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (keyEvent.type == KeyEventType.KeyUp) {
|
||||
when (keyEvent.key) {
|
||||
Key.Escape, Key.Back -> {
|
||||
onQueryChange("")
|
||||
onExpandedChange(false)
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
true
|
||||
}
|
||||
Key.Enter, Key.NumPadEnter -> {
|
||||
keyboardController?.hide()
|
||||
onExpandedChange(false)
|
||||
focusManager.clearFocus()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} else false
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
),
|
||||
cursorBrush = SolidColor(EdenTheme.colors.primary),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
keyboardController?.hide()
|
||||
onExpandedChange(false)
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier.padding(vertical = 14.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
if (query.isEmpty()) {
|
||||
androidx.compose.foundation.text.BasicText(
|
||||
text = searchHint,
|
||||
style = TextStyle(
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
fontSize = 16.sp,
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Clear/Close button (X icon)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
onQueryChange("")
|
||||
onExpandedChange(false)
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_clear),
|
||||
contentDescription = context.getString(R.string.home_clear),
|
||||
tint = Color.White.copy(alpha = 0.7f),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
} else {
|
||||
// Collapsed state: search icon
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (isIconFocused) EdenTheme.colors.primary
|
||||
else EdenTheme.colors.surface
|
||||
)
|
||||
.focusRequester(focusRequester)
|
||||
.focusable(interactionSource = iconInteractionSource)
|
||||
.clickable(
|
||||
interactionSource = iconInteractionSource,
|
||||
indication = null,
|
||||
) {
|
||||
if (hasActiveFilter) {
|
||||
onQueryChange("")
|
||||
} else {
|
||||
onExpandedChange(true)
|
||||
}
|
||||
}
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key in ConfirmKeys) {
|
||||
if (hasActiveFilter) {
|
||||
onQueryChange("")
|
||||
} else {
|
||||
onExpandedChange(true)
|
||||
}
|
||||
true
|
||||
} else false
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
|
||||
contentDescription = context.getString(R.string.home_search),
|
||||
tint = if (hasActiveFilter) EdenTheme.colors.primary else Color.White,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
// Active filter indicator dot
|
||||
if (hasActiveFilter) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(6.dp)
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(EdenTheme.colors.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import androidx.compose.foundation.gestures.snapping.SnapPosition
|
||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun GameCarousel(
|
||||
gameTiles: List<GameTile>,
|
||||
onGameClick: (GameTile) -> Unit,
|
||||
onGameLongClick: (GameTile) -> Unit = {},
|
||||
focusRequester: FocusRequester,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
initialFocusedIndex: Int = 0,
|
||||
onFocusedIndexChanged: (Int) -> Unit = {},
|
||||
onShowGameInfo: (GameTile) -> Unit = {},
|
||||
) {
|
||||
if (gameTiles.isEmpty()) return
|
||||
|
||||
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
|
||||
|
||||
val listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = safeInitialIndex,
|
||||
initialFirstVisibleItemScrollOffset = 0
|
||||
)
|
||||
val snapFlingBehavior = rememberSnapFlingBehavior(
|
||||
lazyListState = listState,
|
||||
snapPosition = SnapPosition.Center
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var centerItemIndex by remember { mutableIntStateOf(safeInitialIndex) }
|
||||
|
||||
LaunchedEffect(centerItemIndex) {
|
||||
onFocusedIndexChanged(centerItemIndex)
|
||||
}
|
||||
|
||||
suspend fun centerOnIndex(index: Int, animate: Boolean = true) {
|
||||
if (gameTiles.isEmpty()) return
|
||||
if (listState.layoutInfo.visibleItemsInfo.none { it.index == index }) {
|
||||
if (animate) {
|
||||
listState.animateScrollToItem(index)
|
||||
} else {
|
||||
listState.scrollToItem(index)
|
||||
}
|
||||
}
|
||||
delay(10)
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return
|
||||
val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2
|
||||
val desiredStart = viewportCenter - itemInfo.size / 2
|
||||
if (itemInfo.offset != desiredStart) {
|
||||
val scrollOffset = -(viewportCenter - itemInfo.size / 2)
|
||||
if (animate) {
|
||||
listState.animateScrollToItem(index, scrollOffset = scrollOffset)
|
||||
} else {
|
||||
listState.scrollToItem(index, scrollOffset = scrollOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
listState.scrollToItem(safeInitialIndex)
|
||||
delay(50)
|
||||
centerOnIndex(safeInitialIndex, animate = false)
|
||||
}
|
||||
|
||||
val focusRequesters = remember(gameTiles.size) {
|
||||
List(gameTiles.size) { FocusRequester() }
|
||||
}
|
||||
|
||||
var isFocusTriggeredScroll by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.isScrollInProgress }
|
||||
.collect { isScrolling ->
|
||||
if (!isScrolling && !isFocusTriggeredScroll && gameTiles.isNotEmpty()) {
|
||||
val firstVisible = listState.firstVisibleItemIndex
|
||||
val scrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val firstVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull()
|
||||
val itemSize = firstVisibleItem?.size ?: 1
|
||||
val newCenterIndex = if (scrollOffset > itemSize / 2) {
|
||||
(firstVisible + 1).coerceAtMost(gameTiles.size - 1)
|
||||
} else {
|
||||
firstVisible
|
||||
}
|
||||
if (newCenterIndex != centerItemIndex && newCenterIndex in focusRequesters.indices) {
|
||||
centerItemIndex = newCenterIndex
|
||||
try {
|
||||
focusRequesters[newCenterIndex].requestFocus()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
// Reset the flag when scroll stops
|
||||
if (!isScrolling) {
|
||||
isFocusTriggeredScroll = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carousel settings (matching original CarouselRecyclerView)
|
||||
val borderScale = 0.6f
|
||||
val borderAlpha = 0.35f
|
||||
val overlapFactor = 0.15f
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val screenWidthPx = with(density) { maxWidth.toPx() }
|
||||
val screenHeightPx = with(density) { maxHeight.toPx() }
|
||||
|
||||
val tileTextHeight = 40.dp
|
||||
val tileSpacing = 6.dp
|
||||
|
||||
// Card size
|
||||
val totalTextAreaPx = with(density) { (tileTextHeight + tileSpacing).toPx() }
|
||||
val cardSizePx = (screenHeightPx - totalTextAreaPx) * 0.7f
|
||||
val cardSize = with(density) { cardSizePx.toDp() }
|
||||
val tileHeight = cardSize + tileSpacing + tileTextHeight
|
||||
|
||||
// Horizontal padding to center first/last card
|
||||
val horizontalPaddingPx = (screenWidthPx - cardSizePx) / 2f
|
||||
val horizontalPadding = with(density) { horizontalPaddingPx.toDp() }
|
||||
|
||||
// Negative spacing for overlap, no overlap currently tho
|
||||
val overlapPx = cardSizePx * overlapFactor
|
||||
val itemSpacing = with(density) { (-overlapPx).toDp() }
|
||||
|
||||
val centerPushPx = cardSizePx * 0.12f
|
||||
|
||||
val screenCenterPx = screenWidthPx / 2f
|
||||
|
||||
val itemWidthWithSpacing = cardSizePx - overlapPx
|
||||
|
||||
LazyRow(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
flingBehavior = snapFlingBehavior,
|
||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
contentPadding = PaddingValues(start = horizontalPadding, end = horizontalPadding, top = 30.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
|
||||
val distanceFromCenter by remember {
|
||||
derivedStateOf {
|
||||
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
|
||||
if (itemInfo == null) {
|
||||
1f
|
||||
} else {
|
||||
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
|
||||
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
|
||||
|
||||
val distance = abs(itemCenterInScreen - screenCenterPx)
|
||||
|
||||
(distance / screenCenterPx).coerceIn(0f, 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val shapedScale = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
|
||||
val scale = borderScale + (1f - borderScale) * shapedScale
|
||||
|
||||
val shapedAlpha = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
|
||||
val alpha = borderAlpha + (1f - borderAlpha) * shapedAlpha
|
||||
|
||||
val zIndex = 100f * (1f - distanceFromCenter)
|
||||
|
||||
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
|
||||
val isLeftOfCenter = if (itemInfo != null) {
|
||||
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
|
||||
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
|
||||
itemCenterInScreen < screenCenterPx
|
||||
} else {
|
||||
index < (gameTiles.size / 2)
|
||||
}
|
||||
val pushFactor = (1f - distanceFromCenter) * distanceFromCenter * 4f
|
||||
val horizontalPush = centerPushPx * pushFactor * (if (isLeftOfCenter) -1f else 1f)
|
||||
|
||||
val tileModifier = if (index == centerItemIndex) {
|
||||
Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusRequester(focusRequesters[index])
|
||||
} else {
|
||||
Modifier.focusRequester(focusRequesters[index])
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = cardSize, height = tileHeight)
|
||||
.zIndex(zIndex)
|
||||
.onFocusChanged { focusState ->
|
||||
if (focusState.hasFocus && centerItemIndex != index) {
|
||||
isFocusTriggeredScroll = true
|
||||
centerItemIndex = index
|
||||
coroutineScope.launch {
|
||||
centerOnIndex(index, animate = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (keyEvent.type == KeyEventType.KeyDown) {
|
||||
when (keyEvent.key) {
|
||||
Key.ButtonX -> {
|
||||
onShowGameInfo(tile)
|
||||
true
|
||||
}
|
||||
Key.DirectionUp, Key.DirectionUpLeft, Key.DirectionUpRight -> {
|
||||
onNavigateUp()
|
||||
true
|
||||
}
|
||||
Key.DirectionLeft -> {
|
||||
if (index > 0) {
|
||||
focusRequesters[index - 1].requestFocus()
|
||||
}
|
||||
true
|
||||
}
|
||||
Key.DirectionRight -> {
|
||||
if (index < gameTiles.size - 1) {
|
||||
focusRequesters[index + 1].requestFocus()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
this.alpha = alpha
|
||||
translationX = horizontalPush
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
EdenTile(
|
||||
title = tile.title,
|
||||
iconSize = cardSize,
|
||||
tileHeight = tileHeight,
|
||||
onClick = {
|
||||
if (index == centerItemIndex) {
|
||||
onGameClick(tile)
|
||||
} else {
|
||||
isFocusTriggeredScroll = true
|
||||
centerItemIndex = index
|
||||
coroutineScope.launch {
|
||||
centerOnIndex(index, animate = true)
|
||||
}
|
||||
focusRequesters[index].requestFocus()
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (index == centerItemIndex) {
|
||||
onGameLongClick(tile)
|
||||
} else {
|
||||
isFocusTriggeredScroll = true
|
||||
centerItemIndex = index
|
||||
coroutineScope.launch {
|
||||
centerOnIndex(index, animate = true)
|
||||
onGameLongClick(tile)
|
||||
}
|
||||
focusRequesters[index].requestFocus()
|
||||
}
|
||||
},
|
||||
modifier = tileModifier,
|
||||
imageUri = tile.iconUri,
|
||||
useLargerFont = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun GameGrid(
|
||||
gameTiles: List<GameTile>,
|
||||
onGameClick: (GameTile) -> Unit,
|
||||
onGameLongClick: (GameTile) -> Unit = {},
|
||||
onNavigateToSettings: () -> Unit = {},
|
||||
focusRequester: FocusRequester,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
rowCount: Int = 2,
|
||||
tileIconSize: Dp = 100.dp,
|
||||
paddingTop: Dp = 0.dp,
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false,
|
||||
emptyMessage: String? = null,
|
||||
initialFocusedIndex: Int = 0,
|
||||
onFocusedIndexChanged: (Int) -> Unit = {},
|
||||
onShowGameInfo: (GameTile) -> Unit = {},
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
|
||||
when {
|
||||
isLoading -> LoadingMessage()
|
||||
emptyMessage != null -> EmptyMessage(emptyMessage)
|
||||
else -> {
|
||||
GameGridContent(
|
||||
gameTiles = gameTiles,
|
||||
onGameClick = onGameClick,
|
||||
onGameLongClick = onGameLongClick,
|
||||
focusRequester = focusRequester,
|
||||
onNavigateUp = onNavigateUp,
|
||||
rowCount = rowCount,
|
||||
tileIconSize = tileIconSize,
|
||||
paddingTop = paddingTop,
|
||||
initialFocusedIndex = initialFocusedIndex,
|
||||
onFocusedIndexChanged = onFocusedIndexChanged,
|
||||
onXPress = { index ->
|
||||
gameTiles.getOrNull(index)?.let { tile ->
|
||||
onShowGameInfo(tile)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GameGridContent(
|
||||
gameTiles: List<GameTile>,
|
||||
onGameClick: (GameTile) -> Unit,
|
||||
onGameLongClick: (GameTile) -> Unit = {},
|
||||
focusRequester: FocusRequester,
|
||||
onNavigateUp: () -> Unit,
|
||||
rowCount: Int,
|
||||
tileIconSize: Dp,
|
||||
paddingTop: Dp,
|
||||
initialFocusedIndex: Int = 0,
|
||||
onFocusedIndexChanged: (Int) -> Unit = {},
|
||||
onXPress: (Int) -> Unit = {},
|
||||
) {
|
||||
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
|
||||
var focusedIndex by remember { mutableIntStateOf(safeInitialIndex) }
|
||||
|
||||
val gridState = rememberLazyGridState(
|
||||
initialFirstVisibleItemIndex = safeInitialIndex
|
||||
)
|
||||
|
||||
LaunchedEffect(gameTiles.size) {
|
||||
if (focusedIndex >= gameTiles.size) {
|
||||
focusedIndex = 0
|
||||
gridState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
val tileFocusRequesters = remember(gameTiles.size) {
|
||||
List(gameTiles.size) { FocusRequester() }
|
||||
}
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
if (tileFocusRequesters.isNotEmpty()) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (tileFocusRequesters.isNotEmpty() && safeInitialIndex in tileFocusRequesters.indices) {
|
||||
gridState.scrollToItem(safeInitialIndex)
|
||||
tileFocusRequesters[safeInitialIndex].requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(focusedIndex) {
|
||||
if (focusedIndex in tileFocusRequesters.indices) {
|
||||
tileFocusRequesters[focusedIndex].requestFocus()
|
||||
onFocusedIndexChanged(focusedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
val outerPaddingTop = 62.dp
|
||||
val outerPaddingBottom = 16.dp
|
||||
val outerPaddingHorizontal = 0.dp
|
||||
val gridVerticalSpacing = 0.dp
|
||||
val gridVerticalPadding = 12.dp
|
||||
val spacerHeight = 2.dp
|
||||
val minTextHeight = 42.dp
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = outerPaddingTop,
|
||||
start = outerPaddingHorizontal,
|
||||
end = outerPaddingHorizontal,
|
||||
bottom = outerPaddingBottom
|
||||
)
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (keyEvent.type == KeyEventType.KeyDown && gameTiles.isNotEmpty()) {
|
||||
val currentRow = focusedIndex % rowCount
|
||||
|
||||
when (keyEvent.key) {
|
||||
Key.ButtonX -> {
|
||||
onXPress(focusedIndex)
|
||||
true
|
||||
}
|
||||
Key.DirectionRight, Key.D -> {
|
||||
val nextIndex = focusedIndex + rowCount
|
||||
if (nextIndex < gameTiles.size) {
|
||||
focusedIndex = nextIndex
|
||||
}
|
||||
true
|
||||
}
|
||||
Key.DirectionLeft, Key.A -> {
|
||||
val prevIndex = focusedIndex - rowCount
|
||||
if (prevIndex >= 0) {
|
||||
focusedIndex = prevIndex
|
||||
}
|
||||
true
|
||||
}
|
||||
Key.DirectionDown, Key.S -> {
|
||||
val nextIndex = focusedIndex + 1
|
||||
if (nextIndex < gameTiles.size && nextIndex % rowCount != 0) {
|
||||
focusedIndex = nextIndex
|
||||
}
|
||||
true
|
||||
}
|
||||
Key.DirectionUp, Key.W -> {
|
||||
if (currentRow > 0) {
|
||||
focusedIndex -= 1
|
||||
true
|
||||
} else {
|
||||
onNavigateUp()
|
||||
true
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
val availableHeight = maxHeight
|
||||
val heightForRows = (availableHeight - (gridVerticalPadding * 2) -
|
||||
(gridVerticalSpacing * (rowCount - 1))).coerceAtLeast(0.dp)
|
||||
val perRowHeight = heightForRows.safeDiv(rowCount)
|
||||
val iconMaxSize = tileIconSize * 2
|
||||
val iconSizeForGrid = (perRowHeight - spacerHeight - minTextHeight)
|
||||
.coerceAtLeast(96.dp)
|
||||
.coerceAtMost(iconMaxSize)
|
||||
val tileHeight = perRowHeight.coerceAtLeast(iconSizeForGrid + spacerHeight + minTextHeight)
|
||||
|
||||
LazyHorizontalGrid(
|
||||
rows = GridCells.Fixed(rowCount),
|
||||
state = gridState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = gridVerticalPadding),
|
||||
) {
|
||||
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
|
||||
val tileModifier = if (index == focusedIndex) {
|
||||
Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusRequester(tileFocusRequesters[index])
|
||||
} else {
|
||||
Modifier.focusRequester(tileFocusRequesters[index])
|
||||
}
|
||||
|
||||
EdenTile(
|
||||
title = tile.title,
|
||||
iconSize = iconSizeForGrid,
|
||||
tileHeight = tileHeight,
|
||||
onClick = {
|
||||
focusedIndex = index
|
||||
onGameClick(tile)
|
||||
},
|
||||
onLongClick = {
|
||||
focusedIndex = index
|
||||
onGameLongClick(tile)
|
||||
},
|
||||
modifier = tileModifier,
|
||||
imageUri = tile.iconUri,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingMessage() {
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
color = EdenTheme.colors.primary,
|
||||
)
|
||||
BasicText(
|
||||
text = context.getString(R.string.loading),
|
||||
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyMessage(message: String) {
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
BasicText(
|
||||
text = message,
|
||||
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
|
||||
)
|
||||
BasicText(
|
||||
text = context.getString(R.string.manage_game_folders),
|
||||
style = EdenTheme.typography.label.copy(color = EdenTheme.colors.onBackground),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Dp.safeDiv(divisor: Int): Dp = if (divisor > 0) this / divisor else 0.dp
|
||||
@@ -1,30 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
|
||||
data class GameTile(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val uri: String? = null,
|
||||
val iconUri: String? = null,
|
||||
val programId: Long = 0L,
|
||||
val developer: String = "",
|
||||
val version: String = "",
|
||||
val isHomebrew: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
fun fromGame(game: Game): GameTile = GameTile(
|
||||
id = game.path,
|
||||
title = game.title,
|
||||
uri = game.path,
|
||||
iconUri = game.path,
|
||||
programId = game.programId.toLongOrNull() ?: 0L,
|
||||
developer = game.developer,
|
||||
version = game.version,
|
||||
isHomebrew = game.isHomebrew,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.eden.emu.ui.theme.Dimens
|
||||
import org.yuzu.yuzu_emu.R
|
||||
|
||||
enum class FooterButton(@DrawableRes val iconRes: Int) {
|
||||
A(R.drawable.facebutton_a),
|
||||
B(R.drawable.facebutton_b),
|
||||
X(R.drawable.facebutton_x),
|
||||
Y(R.drawable.facebutton_y),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeFooter(
|
||||
actions: List<FooterAction>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Dimens.paddingLg, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingXl),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
actions.forEach { action ->
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = Dimens.paddingXs, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = action.button.iconRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.9f)),
|
||||
)
|
||||
BasicText(action.text, style = TextStyle(Color.White.copy(0.7f), 14.sp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class FooterAction(val button: FooterButton, val text: String)
|
||||
data class FooterFocusRequesters(val primaryAction: FocusRequester, val secondaryAction: FocusRequester)
|
||||
@@ -1,186 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import dev.eden.emu.ui.theme.Dimens
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import dev.eden.emu.ui.theme.Shapes
|
||||
import dev.eden.emu.ui.utils.ConfirmKeys
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun HomeHeader(
|
||||
currentUser: String,
|
||||
onUserClick: () -> Unit,
|
||||
searchQuery: String,
|
||||
onSearchQueryChange: (String) -> Unit,
|
||||
isSearchExpanded: Boolean,
|
||||
onSearchExpandedChange: (Boolean) -> Unit,
|
||||
onSortClick: () -> Unit,
|
||||
onFilterClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
focusRequesters: HeaderFocusRequesters,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var username by remember { mutableStateOf("Eden") }
|
||||
var imagePath by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(currentUser) {
|
||||
val uuid = currentUser.ifEmpty { NativeLibrary.getCurrentUser() ?: "" }
|
||||
if (uuid.isNotEmpty()) {
|
||||
username = NativeLibrary.getUserUsername(uuid) ?: "Eden"
|
||||
imagePath = NativeLibrary.getUserImagePath(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Dimens.paddingXl, vertical = Dimens.paddingMd),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UserProfileButton(username, imagePath, onUserClick, focusRequesters.userButton)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingLg),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ExpandableSearchBar(searchQuery, onSearchQueryChange, isSearchExpanded, onSearchExpandedChange, focusRequesters.searchButton)
|
||||
HeaderIconButton(R.drawable.ic_sort, context.getString(R.string.home_sort), onSortClick, focusRequesters.sortButton)
|
||||
HeaderIconButton(R.drawable.ic_filter, context.getString(R.string.home_layout), onFilterClick, focusRequesters.filterButton)
|
||||
HeaderIconButton(R.drawable.ic_settings, context.getString(R.string.home_settings), onSettingsClick, focusRequesters.settingsButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileButton(
|
||||
username: String,
|
||||
imagePath: String?,
|
||||
onClick: () -> Unit,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val bitmap = remember(imagePath) {
|
||||
imagePath?.takeIf { it.isNotEmpty() }?.let { path ->
|
||||
File(path).takeIf { it.exists() }?.let { BitmapFactory.decodeFile(path) }
|
||||
}
|
||||
}
|
||||
|
||||
val defaultBitmap = remember {
|
||||
NativeLibrary.getDefaultAccountBackupJpeg().let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusable(interactionSource = interactionSource)
|
||||
.onPreviewKeyEvent { e ->
|
||||
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
|
||||
}
|
||||
.clickable(interactionSource, null, onClick = onClick)
|
||||
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
|
||||
.border(Dimens.borderWidth, if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
|
||||
.padding(horizontal = Dimens.paddingMd, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingMd),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
(bitmap ?: defaultBitmap)?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(46.dp).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
BasicText(username, style = EdenTheme.typography.body.copy(fontSize = 18.sp, color = Color.White))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HeaderIconButton(
|
||||
iconRes: Int,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(Dimens.iconLg)
|
||||
.focusRequester(focusRequester)
|
||||
.focusable(interactionSource = interactionSource)
|
||||
.onPreviewKeyEvent { e ->
|
||||
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
|
||||
}
|
||||
.clip(CircleShape)
|
||||
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface)
|
||||
.clickable(interactionSource, null, onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(iconRes),
|
||||
contentDescription = contentDescription,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(Dimens.iconSm),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class HeaderFocusRequesters(
|
||||
val userButton: FocusRequester,
|
||||
val searchButton: FocusRequester,
|
||||
val sortButton: FocusRequester,
|
||||
val filterButton: FocusRequester,
|
||||
val settingsButton: FocusRequester,
|
||||
)
|
||||
@@ -1,80 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import dev.eden.emu.ui.theme.Dimens
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import dev.eden.emu.ui.theme.Shapes
|
||||
import dev.eden.emu.ui.utils.DialogButton
|
||||
|
||||
enum class LayoutMode { TWO_ROW, CAROUSEL /*, ONE_ROW */ }
|
||||
|
||||
@Composable
|
||||
fun LayoutSelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSelectGrid: () -> Unit,
|
||||
onSelectCarousel: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusRequesters = remember { List(3) { FocusRequester() } }
|
||||
|
||||
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(Shapes.large)
|
||||
.background(EdenTheme.colors.surface)
|
||||
.padding(Dimens.paddingXl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
BasicText(
|
||||
text = context.getString(R.string.home_layout),
|
||||
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.paddingXl))
|
||||
|
||||
DialogButton(
|
||||
text = context.getString(R.string.view_grid),
|
||||
onClick = { onSelectGrid(); onDismiss() },
|
||||
focusRequester = focusRequesters[0],
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.paddingMd))
|
||||
|
||||
DialogButton(
|
||||
text = context.getString(R.string.view_carousel),
|
||||
onClick = { onSelectCarousel(); onDismiss() },
|
||||
focusRequester = focusRequesters[1],
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.paddingXl))
|
||||
|
||||
DialogButton(
|
||||
text = context.getString(R.string.cancel),
|
||||
onClick = onDismiss,
|
||||
focusRequester = focusRequesters[2],
|
||||
isSecondary = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import dev.eden.emu.ui.theme.Dimens
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import dev.eden.emu.ui.theme.Shapes
|
||||
import dev.eden.emu.ui.utils.DialogButton
|
||||
|
||||
enum class SortMode { LAST_PLAYED, ALPHABETICAL }
|
||||
|
||||
@Composable
|
||||
fun SortSelectionDialog(
|
||||
currentSortMode: SortMode,
|
||||
onDismiss: () -> Unit,
|
||||
onSelectSort: (SortMode) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusRequesters = remember { List(3) { FocusRequester() } }
|
||||
|
||||
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(Shapes.large)
|
||||
.background(EdenTheme.colors.surface)
|
||||
.padding(Dimens.paddingXl),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
BasicText(
|
||||
text = context.getString(R.string.home_sort),
|
||||
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.paddingXl))
|
||||
|
||||
DialogButton(
|
||||
text = context.getString(R.string.search_recently_played),
|
||||
onClick = { onSelectSort(SortMode.LAST_PLAYED); onDismiss() },
|
||||
focusRequester = focusRequesters[0],
|
||||
isSelected = currentSortMode == SortMode.LAST_PLAYED,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.paddingMd))
|
||||
|
||||
DialogButton(
|
||||
text = context.getString(R.string.alphabetical),
|
||||
onClick = { onSelectSort(SortMode.ALPHABETICAL); onDismiss() },
|
||||
focusRequester = focusRequesters[1],
|
||||
isSelected = currentSortMode == SortMode.ALPHABETICAL,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.paddingXl))
|
||||
|
||||
DialogButton(
|
||||
text = context.getString(R.string.cancel),
|
||||
onClick = onDismiss,
|
||||
focusRequester = focusRequesters[2],
|
||||
isSecondary = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.key.*
|
||||
import dev.eden.emu.ui.components.HeaderFocusRequesters
|
||||
import dev.eden.emu.ui.utils.NavKeys
|
||||
|
||||
/** Navigation zones for the home screen */
|
||||
enum class NavigationZone { HEADER, CONTENT/*, FOOTER */ }
|
||||
|
||||
/**
|
||||
* Manages focus and navigation between Header and Content zones.
|
||||
* Uses MutableState for reactive UI updates.
|
||||
* (Default Focus Manager breaks and is based on "placement", not suitable)
|
||||
*/
|
||||
class HomeNavigationManager(
|
||||
private val headerFocusRequesters: HeaderFocusRequesters,
|
||||
private val contentFocusRequester: FocusRequester,
|
||||
) {
|
||||
private val _currentZone: MutableState<NavigationZone> = mutableStateOf(NavigationZone.CONTENT)
|
||||
val currentZone: NavigationZone get() = _currentZone.value
|
||||
|
||||
private val _headerIndex: MutableState<Int> = mutableIntStateOf(0)
|
||||
val headerIndex: Int get() = _headerIndex.value
|
||||
|
||||
// Expose state for Compose observation
|
||||
val currentZoneState: MutableState<NavigationZone> get() = _currentZone
|
||||
val headerIndexState: MutableState<Int> get() = _headerIndex
|
||||
|
||||
private val headerElements = listOf(
|
||||
headerFocusRequesters.userButton,
|
||||
headerFocusRequesters.searchButton,
|
||||
headerFocusRequesters.sortButton,
|
||||
headerFocusRequesters.filterButton,
|
||||
headerFocusRequesters.settingsButton,
|
||||
)
|
||||
|
||||
fun navigateUp() {
|
||||
if (_currentZone.value == NavigationZone.CONTENT) {
|
||||
_currentZone.value = NavigationZone.HEADER
|
||||
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateDown() {
|
||||
if (_currentZone.value == NavigationZone.HEADER) {
|
||||
_currentZone.value = NavigationZone.CONTENT
|
||||
contentFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateLeft() {
|
||||
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value > 0) {
|
||||
_headerIndex.value--
|
||||
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateRight() {
|
||||
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value < headerElements.lastIndex) {
|
||||
_headerIndex.value++
|
||||
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestInitialFocus() {
|
||||
_currentZone.value = NavigationZone.CONTENT
|
||||
contentFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
fun resetToContent() {
|
||||
_currentZone.value = NavigationZone.CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle D-Pad navigation for home screen */
|
||||
fun handleHomeNavigation(keyEvent: KeyEvent, nav: HomeNavigationManager): Boolean {
|
||||
if (keyEvent.type != KeyEventType.KeyDown) return false
|
||||
|
||||
return when (keyEvent.key) {
|
||||
in NavKeys.up -> { nav.navigateUp(); true }
|
||||
in NavKeys.down -> { nav.navigateDown(); true }
|
||||
in NavKeys.left -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateLeft(); true } else false
|
||||
in NavKeys.right -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateRight(); true } else false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val LocalEdenTypography = staticCompositionLocalOf {
|
||||
EdenTypography(
|
||||
title = TextStyle(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 20.sp,
|
||||
),
|
||||
body = TextStyle(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
),
|
||||
label = TextStyle(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
object EdenTheme {
|
||||
val colors: EdenColors
|
||||
@Composable get() = LocalEdenColors.current
|
||||
|
||||
val typography: EdenTypography
|
||||
@Composable get() = LocalEdenTypography.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EdenTheme(
|
||||
colors: EdenColors = LocalEdenColors.current,
|
||||
typography: EdenTypography = LocalEdenTypography.current,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalEdenColors provides colors,
|
||||
LocalEdenTypography provides typography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
data class EdenTypography(
|
||||
val title: TextStyle,
|
||||
val body: TextStyle,
|
||||
val label: TextStyle,
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.theme
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val EdenBrandColor = Color(0xFFA161F3)
|
||||
val EdenSplashColor = EdenBrandColor
|
||||
val EdenGridLine = Color(0x2AA161F3)
|
||||
val EdenBackground = Color(0xFF191521)
|
||||
val EdenSurface = Color(0xFF221C2B)
|
||||
val EdenSurfaceVariant = Color(0xFF2D2538)
|
||||
val EdenOnBackground = Color(0xFFEAE5F2)
|
||||
val EdenOnSurface = Color(0xFFEAE5F2)
|
||||
val EdenOnSurfaceVariant = Color(0xFFB0A8BA)
|
||||
val EdenOnPrimary = Color(0xFFFFFFFF)
|
||||
|
||||
internal val LocalEdenColors = staticCompositionLocalOf {
|
||||
EdenColors(
|
||||
background = EdenBackground,
|
||||
surface = EdenSurface,
|
||||
surfaceVariant = EdenSurfaceVariant,
|
||||
primary = EdenBrandColor,
|
||||
onBackground = EdenOnBackground,
|
||||
onSurface = EdenOnSurface,
|
||||
onSurfaceVariant = EdenOnSurfaceVariant,
|
||||
onPrimary = EdenOnPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
data class EdenColors(
|
||||
val background: Color,
|
||||
val surface: Color,
|
||||
val surfaceVariant: Color,
|
||||
val primary: Color,
|
||||
val onBackground: Color,
|
||||
val onSurface: Color,
|
||||
val onSurfaceVariant: Color,
|
||||
val onPrimary: Color,
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// Grid background
|
||||
val EdenGridSpacing = 58.dp
|
||||
val EdenGridLineWidth = 2.dp
|
||||
const val EdenGridSpeedDpPerSecond = 12f
|
||||
|
||||
// Common dimensions
|
||||
object Dimens {
|
||||
// Padding
|
||||
val paddingXs = 4.dp
|
||||
val paddingSm = 8.dp
|
||||
val paddingMd = 12.dp
|
||||
val paddingLg = 16.dp
|
||||
val paddingXl = 24.dp
|
||||
|
||||
// Icon sizes
|
||||
val iconSm = 24.dp
|
||||
val iconMd = 32.dp
|
||||
val iconLg = 48.dp
|
||||
|
||||
// Border
|
||||
val borderWidth = 2.dp
|
||||
|
||||
// Corner radius
|
||||
val radiusSm = 8.dp
|
||||
val radiusMd = 12.dp
|
||||
val radiusLg = 16.dp
|
||||
}
|
||||
|
||||
// Common shapes
|
||||
object Shapes {
|
||||
val small = RoundedCornerShape(Dimens.radiusSm)
|
||||
val medium = RoundedCornerShape(Dimens.radiusMd)
|
||||
val large = RoundedCornerShape(Dimens.radiusLg)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.utils
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.eden.emu.ui.theme.Dimens
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import dev.eden.emu.ui.theme.Shapes
|
||||
|
||||
/**
|
||||
* New dialog button for consistent styling across dialogs
|
||||
*/
|
||||
@Composable
|
||||
fun DialogButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
isSelected: Boolean = false,
|
||||
isSecondary: Boolean = false,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
val backgroundColor = when {
|
||||
isFocused -> EdenTheme.colors.primary
|
||||
isSelected -> EdenTheme.colors.primary.copy(alpha = 0.3f)
|
||||
isSecondary -> Color.Transparent
|
||||
else -> EdenTheme.colors.background
|
||||
}
|
||||
|
||||
val borderColor = when {
|
||||
isFocused || isSelected -> EdenTheme.colors.primary
|
||||
isSecondary -> EdenTheme.colors.onBackground.copy(alpha = 0.3f)
|
||||
else -> EdenTheme.colors.onBackground.copy(alpha = 0.2f)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(Shapes.medium)
|
||||
.background(backgroundColor)
|
||||
.border(Dimens.borderWidth, borderColor, Shapes.medium)
|
||||
.focusRequester(focusRequester)
|
||||
.focusable(interactionSource = interactionSource)
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyUp && event.key in confirmKeys) {
|
||||
onClick()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
.clickable(interactionSource, null, onClick = onClick)
|
||||
.padding(vertical = 14.dp, horizontal = 20.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
BasicText(
|
||||
text = if (isSelected && !isSecondary) "✓ $text" else text,
|
||||
style = EdenTheme.typography.body.copy(color = Color.White, fontSize = 16.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val confirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
|
||||
@@ -1,17 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dev.eden.emu.ui.utils
|
||||
|
||||
import androidx.compose.ui.input.key.Key
|
||||
|
||||
val ConfirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
|
||||
val MenuKeys = setOf(Key.ButtonX, Key.Menu)
|
||||
val BackKeys = setOf(Key.Escape, Key.Back, Key.ButtonB)
|
||||
|
||||
object NavKeys {
|
||||
val up = setOf(Key.DirectionUp, Key.W)
|
||||
val down = setOf(Key.DirectionDown, Key.S)
|
||||
val left = setOf(Key.DirectionLeft, Key.A)
|
||||
val right = setOf(Key.DirectionRight, Key.D)
|
||||
}
|
||||
@@ -203,6 +203,24 @@ object NativeLibrary {
|
||||
|
||||
external fun getDebugKnobAt(index: Int): Boolean
|
||||
|
||||
/**
|
||||
* Set the current speed limit to the configured turbo speed.
|
||||
*/
|
||||
external fun setTurboSpeedLimit(enabled: Boolean)
|
||||
|
||||
/**
|
||||
* Set the current speed limit to the configured slow speed.
|
||||
*/
|
||||
external fun setSlowSpeedLimit(enabled: Boolean)
|
||||
|
||||
/**
|
||||
* Set the current speed limit to the configured standard speed.
|
||||
*/
|
||||
external fun setStandardSpeedLimit(enabled: Boolean)
|
||||
|
||||
external fun isTurboMode(): Boolean
|
||||
external fun isSlowMode(): Boolean
|
||||
|
||||
/**
|
||||
* Returns Vulkan driver version / API version / GPU model
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
@@ -18,8 +18,6 @@ import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.LocaleList
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.DocumentsTree
|
||||
@@ -30,7 +28,7 @@ import java.util.Locale
|
||||
|
||||
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
|
||||
|
||||
class YuzuApplication : Application(), ImageLoaderFactory {
|
||||
class YuzuApplication : Application() {
|
||||
private fun createNotificationChannels() {
|
||||
val name: CharSequence = getString(R.string.app_notification_channel_name)
|
||||
val description = getString(R.string.app_notification_channel_description)
|
||||
@@ -78,12 +76,6 @@ class YuzuApplication : Application(), ImageLoaderFactory {
|
||||
createNotificationChannels()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
.crossfade(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var documentsTree: DocumentsTree? = null
|
||||
lateinit var application: YuzuApplication
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
@@ -649,7 +649,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
|
||||
|
||||
fun launch(activity: AppCompatActivity, game: Game) {
|
||||
val launcher = Intent(activity, EmulationActivity::class.java)
|
||||
launcher.putExtra("game", game) // Use "game" to match navigation argus
|
||||
launcher.putExtra(EXTRA_SELECTED_GAME, game)
|
||||
activity.startActivity(launcher)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.widget.RadioGroup
|
||||
import android.widget.TextView
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||
@@ -42,7 +43,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
||||
statusText.setTextColor(
|
||||
MaterialColors.getColor(
|
||||
statusText,
|
||||
androidx.appcompat.R.attr.colorPrimary
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
)
|
||||
)
|
||||
|
||||
@@ -135,6 +136,39 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
||||
container.addView(itemView)
|
||||
}
|
||||
|
||||
fun addCustomToggle(
|
||||
name: Int,
|
||||
isChecked: Boolean,
|
||||
isEnabled: Boolean,
|
||||
|
||||
container: ViewGroup,
|
||||
callback: (Boolean) -> Unit
|
||||
): MaterialSwitch? {
|
||||
val inflater = LayoutInflater.from(emulationFragment.requireContext())
|
||||
val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false)
|
||||
|
||||
val switchContainer = itemView.findViewById<ViewGroup>(R.id.switch_container)
|
||||
val titleView = itemView.findViewById<TextView>(R.id.switch_title)
|
||||
val switchView = itemView.findViewById<MaterialSwitch>(R.id.setting_switch)
|
||||
|
||||
titleView.text = YuzuApplication.appContext.getString(name)
|
||||
switchContainer.visibility = View.VISIBLE
|
||||
|
||||
switchView.isChecked = isChecked
|
||||
|
||||
switchView.setOnCheckedChangeListener { _, checked ->
|
||||
callback(checked)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
switchContainer.setOnClickListener {
|
||||
switchView.toggle()
|
||||
}
|
||||
container.addView(itemView)
|
||||
|
||||
return switchView
|
||||
}
|
||||
|
||||
fun addSliderSetting(
|
||||
name: Int,
|
||||
container: ViewGroup,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.fetcher
|
||||
@@ -171,7 +171,7 @@ class ReleaseAdapter(
|
||||
iconTint = ColorStateList.valueOf(
|
||||
MaterialColors.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.attr.colorPrimary
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
RENDERER_PROVOKING_VERTEX("provoking_vertex"),
|
||||
RENDERER_DESCRIPTOR_INDEXING("descriptor_indexing"),
|
||||
RENDERER_SAMPLE_SHADING("sample_shading"),
|
||||
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
|
||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||
BLACK_BACKGROUNDS("black_backgrounds"),
|
||||
|
||||
@@ -29,6 +29,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
|
||||
|
||||
RENDERER_DYNA_STATE("dyna_state"),
|
||||
DMA_ACCURACY("dma_accuracy"),
|
||||
FRAME_PACING_MODE("frame_pacing_mode"),
|
||||
AUDIO_OUTPUT_ENGINE("output_engine"),
|
||||
MAX_ANISOTROPY("max_anisotropy"),
|
||||
THEME("theme"),
|
||||
|
||||
+7
-1
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -6,7 +9,10 @@ package org.yuzu.yuzu_emu.features.settings.model
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
enum class ShortSetting(override val key: String) : AbstractShortSetting {
|
||||
RENDERER_SPEED_LIMIT("speed_limit");
|
||||
RENDERER_SPEED_LIMIT("speed_limit"),
|
||||
RENDERER_TURBO_SPEED_LIMIT("turbo_speed_limit"),
|
||||
RENDERER_SLOW_SPEED_LIMIT("slow_speed_limit"),
|
||||
;
|
||||
|
||||
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||
|
||||
import androidx.annotation.ArrayRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
|
||||
class GpuUnswizzleSetting(
|
||||
@StringRes titleId: Int = 0,
|
||||
titleString: String = "",
|
||||
@StringRes descriptionId: Int = 0,
|
||||
descriptionString: String = "",
|
||||
@ArrayRes val textureSizeChoicesId: Int,
|
||||
@ArrayRes val textureSizeValuesId: Int,
|
||||
@ArrayRes val streamSizeChoicesId: Int,
|
||||
@ArrayRes val streamSizeValuesId: Int,
|
||||
@ArrayRes val chunkSizeChoicesId: Int,
|
||||
@ArrayRes val chunkSizeValuesId: Int
|
||||
) : SettingsItem(
|
||||
object : AbstractSetting {
|
||||
override val key: String = SettingsItem.GPU_UNSWIZZLE_COMBINED
|
||||
override val defaultValue: Any = false
|
||||
override val isSaveable = true
|
||||
override val isRuntimeModifiable = true
|
||||
override val isSwitchable = true
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = "combined"
|
||||
override fun reset() {
|
||||
BooleanSetting.GPU_UNSWIZZLE_ENABLED.reset()
|
||||
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.reset()
|
||||
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.reset()
|
||||
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.reset()
|
||||
}
|
||||
},
|
||||
titleId,
|
||||
titleString,
|
||||
descriptionId,
|
||||
descriptionString
|
||||
) {
|
||||
override val type = SettingsItem.TYPE_GPU_UNSWIZZLE
|
||||
|
||||
// Check if GPU unswizzle is enabled via the dedicated boolean setting
|
||||
fun isEnabled(needsGlobal: Boolean = false): Boolean =
|
||||
BooleanSetting.GPU_UNSWIZZLE_ENABLED.getBoolean(needsGlobal)
|
||||
|
||||
fun setEnabled(value: Boolean) =
|
||||
BooleanSetting.GPU_UNSWIZZLE_ENABLED.setBoolean(value)
|
||||
|
||||
fun enable() = setEnabled(true)
|
||||
|
||||
fun disable() = setEnabled(false)
|
||||
|
||||
fun getTextureSize(needsGlobal: Boolean = false): Int =
|
||||
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.getInt(needsGlobal)
|
||||
|
||||
fun setTextureSize(value: Int) =
|
||||
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.setInt(value)
|
||||
|
||||
fun getStreamSize(needsGlobal: Boolean = false): Int =
|
||||
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.getInt(needsGlobal)
|
||||
|
||||
fun setStreamSize(value: Int) =
|
||||
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.setInt(value)
|
||||
|
||||
fun getChunkSize(needsGlobal: Boolean = false): Int =
|
||||
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.getInt(needsGlobal)
|
||||
|
||||
fun setChunkSize(value: Int) =
|
||||
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.setInt(value)
|
||||
|
||||
fun reset() = setting.reset()
|
||||
}
|
||||
+43
@@ -104,8 +104,10 @@ abstract class SettingsItem(
|
||||
const val TYPE_SPINBOX = 12
|
||||
const val TYPE_LAUNCHABLE = 13
|
||||
const val TYPE_PATH = 14
|
||||
const val TYPE_GPU_UNSWIZZLE = 15
|
||||
|
||||
const val FASTMEM_COMBINED = "fastmem_combined"
|
||||
const val GPU_UNSWIZZLE_COMBINED = "gpu_unswizzle_combined"
|
||||
|
||||
val emptySetting = object : AbstractSetting {
|
||||
override val key: String = ""
|
||||
@@ -178,6 +180,26 @@ abstract class SettingsItem(
|
||||
units = "%"
|
||||
)
|
||||
)
|
||||
put(
|
||||
SliderSetting(
|
||||
ShortSetting.RENDERER_TURBO_SPEED_LIMIT,
|
||||
titleId = R.string.turbo_speed_limit,
|
||||
descriptionId = R.string.turbo_speed_limit_description,
|
||||
min = 1,
|
||||
max = 400,
|
||||
units = "%"
|
||||
)
|
||||
)
|
||||
put(
|
||||
SliderSetting(
|
||||
ShortSetting.RENDERER_SLOW_SPEED_LIMIT,
|
||||
titleId = R.string.slow_speed_limit,
|
||||
descriptionId = R.string.slow_speed_limit_description,
|
||||
min = 1,
|
||||
max = 400,
|
||||
units = "%"
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.CPU_BACKEND,
|
||||
@@ -641,6 +663,15 @@ abstract class SettingsItem(
|
||||
valuesId = R.array.dmaAccuracyValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.FRAME_PACING_MODE,
|
||||
titleId = R.string.frame_pacing_mode,
|
||||
descriptionId = R.string.frame_pacing_mode_description,
|
||||
choicesId = R.array.framePacingModeNames,
|
||||
valuesId = R.array.framePacingModeValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
|
||||
@@ -684,6 +715,18 @@ abstract class SettingsItem(
|
||||
valuesId = R.array.gpuSwizzleChunkValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
GpuUnswizzleSetting(
|
||||
titleId = R.string.gpu_unswizzle_settings,
|
||||
descriptionId = R.string.gpu_unswizzle_settings_description,
|
||||
textureSizeChoicesId = R.array.gpuTextureSizeSwizzleEntries,
|
||||
textureSizeValuesId = R.array.gpuTextureSizeSwizzleValues,
|
||||
streamSizeChoicesId = R.array.gpuSwizzleEntries,
|
||||
streamSizeValuesId = R.array.gpuSwizzleValues,
|
||||
chunkSizeChoicesId = R.array.gpuSwizzleChunkEntries,
|
||||
chunkSizeValuesId = R.array.gpuSwizzleChunkValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.FAST_CPU_TIME,
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogGpuUnswizzleBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.GpuUnswizzleSetting
|
||||
|
||||
class GpuUnswizzleDialogFragment : DialogFragment() {
|
||||
private var position = 0
|
||||
private val settingsViewModel: SettingsViewModel by activityViewModels()
|
||||
private lateinit var binding: DialogGpuUnswizzleBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
position = requireArguments().getInt(POSITION)
|
||||
|
||||
if (settingsViewModel.clickedItem == null) dismiss()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
binding = DialogGpuUnswizzleBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
val item = settingsViewModel.clickedItem as GpuUnswizzleSetting
|
||||
|
||||
// Setup texture size dropdown
|
||||
val textureSizeEntries = resources.getStringArray(item.textureSizeChoicesId)
|
||||
val textureSizeValues = resources.getIntArray(item.textureSizeValuesId)
|
||||
val textureSizeAdapter = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
textureSizeEntries.toMutableList()
|
||||
)
|
||||
binding.dropdownTextureSize.setAdapter(textureSizeAdapter)
|
||||
|
||||
// Setup stream size dropdown
|
||||
val streamSizeEntries = resources.getStringArray(item.streamSizeChoicesId)
|
||||
val streamSizeValues = resources.getIntArray(item.streamSizeValuesId)
|
||||
val streamSizeAdapter = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
streamSizeEntries.toMutableList()
|
||||
)
|
||||
binding.dropdownStreamSize.setAdapter(streamSizeAdapter)
|
||||
|
||||
// Setup chunk size dropdown
|
||||
val chunkSizeEntries = resources.getStringArray(item.chunkSizeChoicesId)
|
||||
val chunkSizeValues = resources.getIntArray(item.chunkSizeValuesId)
|
||||
val chunkSizeAdapter = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
chunkSizeEntries.toMutableList()
|
||||
)
|
||||
binding.dropdownChunkSize.setAdapter(chunkSizeAdapter)
|
||||
|
||||
// Load current values
|
||||
val isEnabled = item.isEnabled()
|
||||
binding.switchEnable.isChecked = isEnabled
|
||||
|
||||
if (isEnabled) {
|
||||
val textureSizeIndex = textureSizeValues.indexOf(item.getTextureSize())
|
||||
if (textureSizeIndex >= 0) {
|
||||
binding.dropdownTextureSize.setText(textureSizeEntries[textureSizeIndex], false)
|
||||
}
|
||||
|
||||
val streamSizeIndex = streamSizeValues.indexOf(item.getStreamSize())
|
||||
if (streamSizeIndex >= 0) {
|
||||
binding.dropdownStreamSize.setText(streamSizeEntries[streamSizeIndex], false)
|
||||
}
|
||||
|
||||
val chunkSizeIndex = chunkSizeValues.indexOf(item.getChunkSize())
|
||||
if (chunkSizeIndex >= 0) {
|
||||
binding.dropdownChunkSize.setText(chunkSizeEntries[chunkSizeIndex], false)
|
||||
}
|
||||
} else {
|
||||
// Set default/recommended values when disabling
|
||||
binding.dropdownTextureSize.setText(textureSizeEntries[3], false)
|
||||
binding.dropdownStreamSize.setText(streamSizeEntries[3], false)
|
||||
binding.dropdownChunkSize.setText(chunkSizeEntries[3], false)
|
||||
}
|
||||
|
||||
// Clear adapter filters after setText to fix rotation bug
|
||||
textureSizeAdapter.filter.filter(null)
|
||||
streamSizeAdapter.filter.filter(null)
|
||||
chunkSizeAdapter.filter.filter(null)
|
||||
|
||||
// Enable/disable dropdowns based on switch state
|
||||
updateDropdownsState(isEnabled)
|
||||
binding.switchEnable.setOnCheckedChangeListener { _, checked ->
|
||||
updateDropdownsState(checked)
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(item.title)
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
// Setup button listeners
|
||||
binding.btnDefault.setOnClickListener {
|
||||
// Reset to defaults
|
||||
item.reset()
|
||||
// Refresh values with adapters reset
|
||||
val textureSizeIndex = textureSizeValues.indexOf(item.getTextureSize())
|
||||
if (textureSizeIndex >= 0) {
|
||||
binding.dropdownTextureSize.setText(textureSizeEntries[textureSizeIndex], false)
|
||||
}
|
||||
val streamSizeIndex = streamSizeValues.indexOf(item.getStreamSize())
|
||||
if (streamSizeIndex >= 0) {
|
||||
binding.dropdownStreamSize.setText(streamSizeEntries[streamSizeIndex], false)
|
||||
}
|
||||
val chunkSizeIndex = chunkSizeValues.indexOf(item.getChunkSize())
|
||||
if (chunkSizeIndex >= 0) {
|
||||
binding.dropdownChunkSize.setText(chunkSizeEntries[chunkSizeIndex], false)
|
||||
}
|
||||
// Clear filters
|
||||
textureSizeAdapter.filter.filter(null)
|
||||
streamSizeAdapter.filter.filter(null)
|
||||
chunkSizeAdapter.filter.filter(null)
|
||||
|
||||
settingsViewModel.setAdapterItemChanged(position)
|
||||
settingsViewModel.setShouldReloadSettingsList(true)
|
||||
}
|
||||
|
||||
binding.btnCancel.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
binding.btnOk.setOnClickListener {
|
||||
if (binding.switchEnable.isChecked) {
|
||||
item.enable()
|
||||
// Save the selected values
|
||||
val selectedTextureIndex = textureSizeEntries.indexOf(
|
||||
binding.dropdownTextureSize.text.toString()
|
||||
)
|
||||
if (selectedTextureIndex >= 0) {
|
||||
item.setTextureSize(textureSizeValues[selectedTextureIndex])
|
||||
}
|
||||
|
||||
val selectedStreamIndex = streamSizeEntries.indexOf(
|
||||
binding.dropdownStreamSize.text.toString()
|
||||
)
|
||||
if (selectedStreamIndex >= 0) {
|
||||
item.setStreamSize(streamSizeValues[selectedStreamIndex])
|
||||
}
|
||||
|
||||
val selectedChunkIndex = chunkSizeEntries.indexOf(
|
||||
binding.dropdownChunkSize.text.toString()
|
||||
)
|
||||
if (selectedChunkIndex >= 0) {
|
||||
item.setChunkSize(chunkSizeValues[selectedChunkIndex])
|
||||
}
|
||||
} else {
|
||||
// Disable GPU unswizzle
|
||||
item.disable()
|
||||
}
|
||||
|
||||
settingsViewModel.setAdapterItemChanged(position)
|
||||
settingsViewModel.setShouldReloadSettingsList(true)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
// Ensure filters are cleared after dialog is shown
|
||||
binding.root.post {
|
||||
textureSizeAdapter.filter.filter(null)
|
||||
streamSizeAdapter.filter.filter(null)
|
||||
chunkSizeAdapter.filter.filter(null)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun updateDropdownsState(enabled: Boolean) {
|
||||
binding.layoutTextureSize.isEnabled = enabled
|
||||
binding.dropdownTextureSize.isEnabled = enabled
|
||||
binding.layoutStreamSize.isEnabled = enabled
|
||||
binding.dropdownStreamSize.isEnabled = enabled
|
||||
binding.layoutChunkSize.isEnabled = enabled
|
||||
binding.dropdownChunkSize.isEnabled = enabled
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "GpuUnswizzleDialogFragment"
|
||||
const val POSITION = "Position"
|
||||
|
||||
fun newInstance(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
item: GpuUnswizzleSetting,
|
||||
position: Int
|
||||
): GpuUnswizzleDialogFragment {
|
||||
val dialog = GpuUnswizzleDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putInt(POSITION, position)
|
||||
dialog.arguments = args
|
||||
settingsViewModel.clickedItem = item
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.ui
|
||||
@@ -101,6 +101,11 @@ class SettingsAdapter(
|
||||
SettingsItem.TYPE_PATH -> {
|
||||
PathViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_GPU_UNSWIZZLE -> {
|
||||
GpuUnswizzleViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
|
||||
}
|
||||
@@ -474,6 +479,14 @@ class SettingsAdapter(
|
||||
settingsViewModel.setShouldShowPathResetDialog(true)
|
||||
}
|
||||
|
||||
fun onGpuUnswizzleClick(item: GpuUnswizzleSetting, position: Int) {
|
||||
GpuUnswizzleDialogFragment.newInstance(
|
||||
settingsViewModel,
|
||||
item,
|
||||
position
|
||||
).show(fragment.childFragmentManager, GpuUnswizzleDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
|
||||
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
|
||||
return oldItem.setting.key == newItem.setting.key
|
||||
|
||||
+4
-3
@@ -227,6 +227,8 @@ class SettingsFragmentPresenter(
|
||||
add(StringSetting.DEVICE_NAME.key)
|
||||
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
|
||||
add(ShortSetting.RENDERER_SPEED_LIMIT.key)
|
||||
add(ShortSetting.RENDERER_TURBO_SPEED_LIMIT.key)
|
||||
add(ShortSetting.RENDERER_SLOW_SPEED_LIMIT.key)
|
||||
add(BooleanSetting.USE_DOCKED_MODE.key)
|
||||
add(IntSetting.REGION_INDEX.key)
|
||||
add(IntSetting.LANGUAGE_INDEX.key)
|
||||
@@ -265,6 +267,7 @@ class SettingsFragmentPresenter(
|
||||
|
||||
add(IntSetting.RENDERER_ACCURACY.key)
|
||||
add(IntSetting.DMA_ACCURACY.key)
|
||||
add(IntSetting.FRAME_PACING_MODE.key)
|
||||
add(IntSetting.MAX_ANISOTROPY.key)
|
||||
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
|
||||
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
|
||||
@@ -282,9 +285,7 @@ class SettingsFragmentPresenter(
|
||||
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
|
||||
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
|
||||
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
|
||||
add(IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.key)
|
||||
add(IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.key)
|
||||
add(IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.key)
|
||||
add(SettingsItem.GPU_UNSWIZZLE_COMBINED)
|
||||
|
||||
add(HeaderSetting(R.string.extensions))
|
||||
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
|
||||
|
||||
import android.view.View
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.GpuUnswizzleSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
|
||||
class GpuUnswizzleViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||
SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var setting: GpuUnswizzleSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as GpuUnswizzleSetting
|
||||
binding.textSettingName.text = setting.title
|
||||
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
|
||||
binding.textSettingDescription.text = item.description
|
||||
|
||||
binding.textSettingValue.setVisible(true)
|
||||
val resMgr = binding.root.context.resources
|
||||
|
||||
if (setting.isEnabled()) {
|
||||
// Show a summary of current settings
|
||||
val textureSizeEntries = resMgr.getStringArray(setting.textureSizeChoicesId)
|
||||
val textureSizeValues = resMgr.getIntArray(setting.textureSizeValuesId)
|
||||
val textureSizeIndex = textureSizeValues.indexOf(setting.getTextureSize())
|
||||
val textureSizeLabel = if (textureSizeIndex >= 0) textureSizeEntries[textureSizeIndex] else "?"
|
||||
|
||||
val streamSizeEntries = resMgr.getStringArray(setting.streamSizeChoicesId)
|
||||
val streamSizeValues = resMgr.getIntArray(setting.streamSizeValuesId)
|
||||
val streamSizeIndex = streamSizeValues.indexOf(setting.getStreamSize())
|
||||
val streamSizeLabel = if (streamSizeIndex >= 0) streamSizeEntries[streamSizeIndex] else "?"
|
||||
|
||||
val chunkSizeEntries = resMgr.getStringArray(setting.chunkSizeChoicesId)
|
||||
val chunkSizeValues = resMgr.getIntArray(setting.chunkSizeValuesId)
|
||||
val chunkSizeIndex = chunkSizeValues.indexOf(setting.getChunkSize())
|
||||
val chunkSizeLabel = if (chunkSizeIndex >= 0) chunkSizeEntries[chunkSizeIndex] else "?"
|
||||
|
||||
binding.textSettingValue.text = "$textureSizeLabel ⋅ $streamSizeLabel ⋅ $chunkSizeLabel"
|
||||
} else {
|
||||
binding.textSettingValue.text = resMgr.getString(R.string.gpu_unswizzle_disabled)
|
||||
}
|
||||
|
||||
binding.buttonClear.setVisible(setting.clearable)
|
||||
binding.buttonClear.setOnClickListener {
|
||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
setStyle(setting.isEditable, binding)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (!setting.isEditable) {
|
||||
return
|
||||
}
|
||||
|
||||
adapter.onGpuUnswizzleClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
return adapter.onLongClick(setting, bindingAdapterPosition)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -1055,11 +1056,47 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
quickSettings.addPerGameConfigStatusIndicator(container)
|
||||
}
|
||||
|
||||
quickSettings.addBooleanSetting(
|
||||
lateinit var slowSpeed: MaterialSwitch
|
||||
lateinit var turboSpeed: MaterialSwitch
|
||||
|
||||
turboSpeed = quickSettings.addCustomToggle(
|
||||
R.string.turbo_speed_limit,
|
||||
NativeLibrary.isTurboMode(),
|
||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
|
||||
container
|
||||
) { enabled ->
|
||||
if (enabled)
|
||||
slowSpeed.isChecked = false
|
||||
NativeLibrary.setTurboSpeedLimit(enabled)
|
||||
}!!
|
||||
|
||||
slowSpeed = quickSettings.addCustomToggle(
|
||||
R.string.slow_speed_limit,
|
||||
NativeLibrary.isSlowMode(),
|
||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
|
||||
container
|
||||
) { enabled ->
|
||||
if (enabled)
|
||||
turboSpeed.isChecked = false
|
||||
NativeLibrary.setSlowSpeedLimit(enabled)
|
||||
}!!
|
||||
|
||||
quickSettings.addCustomToggle(
|
||||
R.string.frame_limit_enable,
|
||||
container,
|
||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
|
||||
)
|
||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
|
||||
true,
|
||||
container
|
||||
) { enabled ->
|
||||
if (!enabled) {
|
||||
turboSpeed.isChecked = false
|
||||
slowSpeed.isChecked = false
|
||||
}
|
||||
|
||||
turboSpeed.isEnabled = enabled
|
||||
slowSpeed.isEnabled = enabled
|
||||
|
||||
NativeLibrary.setStandardSpeedLimit(enabled)
|
||||
}!!
|
||||
|
||||
quickSettings.addSliderSetting(
|
||||
R.string.frame_limit_slider,
|
||||
@@ -1310,7 +1347,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
setTextColor(
|
||||
MaterialColors.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.attr.colorPrimary
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1497,7 +1534,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
setTextColor(
|
||||
MaterialColors.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.attr.colorPrimary
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,26 +61,16 @@ class GamesViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun setGames(games: List<Game>) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
|
||||
val sortedList = games.sortedWith(
|
||||
compareByDescending<Game> { game ->
|
||||
preferences.getLong(game.keyLastPlayedTime, 0L)
|
||||
}.thenBy { it.title.lowercase(Locale.getDefault()) }
|
||||
.thenBy { it.path }
|
||||
compareBy(
|
||||
{ it.title.lowercase(Locale.getDefault()) },
|
||||
{ it.path }
|
||||
)
|
||||
)
|
||||
|
||||
_games.value = sortedList
|
||||
}
|
||||
|
||||
fun resortGames() {
|
||||
val currentGames = _games.value
|
||||
if (currentGames.isNotEmpty()) {
|
||||
setGames(currentGames)
|
||||
_shouldScrollToTop.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun setShouldSwapData(shouldSwap: Boolean) {
|
||||
_shouldSwapData.value = shouldSwap
|
||||
}
|
||||
|
||||
@@ -3,468 +3,565 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.unit.dp
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import java.util.Locale
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.delay
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||
import org.yuzu.yuzu_emu.model.AppletInfo
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import dev.eden.emu.ui.background.RetroGridBackground
|
||||
import dev.eden.emu.ui.components.FooterAction
|
||||
import dev.eden.emu.ui.components.FooterButton
|
||||
import dev.eden.emu.ui.components.GameCarousel
|
||||
import dev.eden.emu.ui.components.GameGrid
|
||||
import dev.eden.emu.ui.components.GameTile
|
||||
import dev.eden.emu.ui.components.HeaderFocusRequesters
|
||||
import dev.eden.emu.ui.components.HomeFooter
|
||||
import dev.eden.emu.ui.components.HomeHeader
|
||||
import dev.eden.emu.ui.components.LayoutMode
|
||||
import dev.eden.emu.ui.components.LayoutSelectionDialog
|
||||
import dev.eden.emu.ui.components.SortMode
|
||||
import dev.eden.emu.ui.components.SortSelectionDialog
|
||||
import dev.eden.emu.ui.navigation.HomeNavigationManager
|
||||
import dev.eden.emu.ui.navigation.NavigationZone
|
||||
import dev.eden.emu.ui.navigation.handleHomeNavigation
|
||||
import dev.eden.emu.ui.theme.EdenTheme
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
import org.yuzu.yuzu_emu.utils.collect
|
||||
import info.debatty.java.stringsimilarity.Jaccard
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import java.util.Locale
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.doOnNextLayout
|
||||
|
||||
private const val PREF_LAYOUT_MODE = "home_layout_mode"
|
||||
private const val PREF_SORT_MODE = "home_sort_mode"
|
||||
private const val PREF_LAST_FOCUSED_INDEX = "home_last_focused_index"
|
||||
private const val PREF_GAME_WAS_LAUNCHED = "home_game_was_launched"
|
||||
|
||||
/**
|
||||
* Compose-based Games Fragment with Header/Footer navigation
|
||||
*/
|
||||
class GamesFragment : Fragment() {
|
||||
private var _binding: FragmentGamesBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var originalHeaderTopMargin: Int? = null
|
||||
private var originalHeaderBottomMargin: Int? = null
|
||||
private var originalHeaderRightMargin: Int? = null
|
||||
private var originalHeaderLeftMargin: Int? = null
|
||||
|
||||
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
|
||||
private var fallbackBottomInset: Int = 0
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_TEXT = "SearchText"
|
||||
private const val PREF_SORT_TYPE = "GamesSortType"
|
||||
}
|
||||
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private lateinit var gameAdapter: GameAdapter
|
||||
|
||||
private val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
|
||||
private lateinit var mainActivity: MainActivity
|
||||
private val getGamesDirectory =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||
if (result != null) {
|
||||
mainActivity.processGamesDir(result, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentViewType(): Int {
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
|
||||
val fallback = if (isLandscape) GameAdapter.VIEW_TYPE_CAROUSEL else GameAdapter.VIEW_TYPE_GRID
|
||||
return preferences.getInt(key, fallback)
|
||||
}
|
||||
|
||||
private fun setCurrentViewType(type: Int) {
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
|
||||
preferences.edit { putInt(key, type) }
|
||||
}
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
EdenTheme {
|
||||
HomeScreen(
|
||||
gamesViewModel = gamesViewModel,
|
||||
onGameLaunch = { path: String ->
|
||||
val game = gamesViewModel.games.value.find { it.path == path }
|
||||
if (game != null) {
|
||||
// Save last played time for sorting
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.edit()
|
||||
.putLong(game.keyLastPlayedTime, System.currentTimeMillis())
|
||||
.putBoolean(PREF_GAME_WAS_LAUNCHED, true)
|
||||
.apply()
|
||||
|
||||
EmulationActivity.launch(
|
||||
requireActivity() as androidx.appcompat.app.AppCompatActivity,
|
||||
game
|
||||
)
|
||||
}
|
||||
},
|
||||
onNavigateToSettings = {
|
||||
findNavController().navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
|
||||
},
|
||||
onNavigateToUserManagement = {
|
||||
findNavController().navigate(R.id.action_gamesFragment_to_profileManagerFragment)
|
||||
},
|
||||
onShowGameInfo = { game: Game ->
|
||||
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
_binding = FragmentGamesBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setStatusBarShadeVisibility(true)
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
|
||||
}
|
||||
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity
|
||||
)
|
||||
|
||||
applyGridGamesBinding()
|
||||
|
||||
binding.swipeRefresh.apply {
|
||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setOnRefreshListener {
|
||||
gamesViewModel.reloadGames(false)
|
||||
}
|
||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressBackgroundColorSchemeColor(
|
||||
com.google.android.material.color.MaterialColors.getColor(
|
||||
binding.swipeRefresh,
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
)
|
||||
)
|
||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setColorSchemeColors(
|
||||
com.google.android.material.color.MaterialColors.getColor(
|
||||
binding.swipeRefresh,
|
||||
com.google.android.material.R.attr.colorOnPrimary
|
||||
)
|
||||
)
|
||||
post {
|
||||
if (_binding == null) {
|
||||
return@post
|
||||
}
|
||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = gamesViewModel.isReloading.value
|
||||
}
|
||||
}
|
||||
|
||||
gamesViewModel.isReloading.collect(viewLifecycleOwner) {
|
||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = it
|
||||
binding.noticeText.setVisible(
|
||||
visible = gamesViewModel.games.value.isEmpty() && !it,
|
||||
gone = false
|
||||
)
|
||||
}
|
||||
gamesViewModel.games.collect(viewLifecycleOwner) {
|
||||
if (it.isNotEmpty()) {
|
||||
setAdapter(it)
|
||||
}
|
||||
}
|
||||
gamesViewModel.shouldSwapData.collect(
|
||||
viewLifecycleOwner,
|
||||
resetState = { gamesViewModel.setShouldSwapData(false) }
|
||||
) {
|
||||
if (it) {
|
||||
setAdapter(gamesViewModel.games.value)
|
||||
}
|
||||
}
|
||||
gamesViewModel.shouldScrollToTop.collect(
|
||||
viewLifecycleOwner,
|
||||
resetState = { gamesViewModel.setShouldScrollToTop(false) }
|
||||
) { if (it) scrollToTop() }
|
||||
|
||||
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
|
||||
if (shouldScroll) {
|
||||
binding.gridGames.post {
|
||||
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
|
||||
gameAdapter.notifyDataSetChanged()
|
||||
}
|
||||
gamesViewModel.setShouldScrollAfterReload(false)
|
||||
}
|
||||
}
|
||||
|
||||
setupTopView()
|
||||
|
||||
updateButtonsVisibility()
|
||||
|
||||
binding.addDirectory.setOnClickListener {
|
||||
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||
}
|
||||
|
||||
binding.launchQlaunch?.setOnClickListener {
|
||||
launchQLaunch()
|
||||
}
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
val applyGridGamesBinding = {
|
||||
(binding.gridGames as? RecyclerView)?.apply {
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val currentViewType = getCurrentViewType()
|
||||
val savedViewType = if (isLandscape || currentViewType != GameAdapter.VIEW_TYPE_CAROUSEL) currentViewType else GameAdapter.VIEW_TYPE_GRID
|
||||
|
||||
//This prevents Grid/List views from reusing scaled or otherwise modified ViewHolders left over from the carousel.
|
||||
adapter = null
|
||||
recycledViewPool.clear()
|
||||
|
||||
gameAdapter.setViewType(savedViewType)
|
||||
currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
|
||||
|
||||
// Set the correct layout manager
|
||||
layoutManager = when (savedViewType) {
|
||||
GameAdapter.VIEW_TYPE_GRID -> {
|
||||
val columns = resources.getInteger(R.integer.game_columns_grid)
|
||||
GridLayoutManager(context, columns)
|
||||
}
|
||||
GameAdapter.VIEW_TYPE_GRID_COMPACT -> {
|
||||
val columns = resources.getInteger(R.integer.game_columns_grid)
|
||||
GridLayoutManager(context, columns)
|
||||
}
|
||||
GameAdapter.VIEW_TYPE_LIST -> {
|
||||
val columns = resources.getInteger(R.integer.game_columns_list)
|
||||
GridLayoutManager(context, columns)
|
||||
}
|
||||
GameAdapter.VIEW_TYPE_CAROUSEL -> {
|
||||
LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid view type: $savedViewType")
|
||||
}
|
||||
if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
(binding.gridGames as? View)?.let { it -> ViewCompat.requestApplyInsets(it)}
|
||||
doOnNextLayout { //Carousel: important to avoid overlap issues
|
||||
(this as? CarouselRecyclerView)?.notifyLaidOut(fallbackBottomInset)
|
||||
}
|
||||
} else {
|
||||
(this as? CarouselRecyclerView)?.setupCarousel(false)
|
||||
}
|
||||
adapter = gameAdapter
|
||||
lastViewType = savedViewType
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
if (_binding != null) {
|
||||
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
gamesViewModel.lastScrollPosition = (binding.gridGames as? CarouselRecyclerView)?.getClosestChildPosition() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Only resort games if a game was actually launched (not when coming back from settings)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
if (prefs.getBoolean(PREF_GAME_WAS_LAUNCHED, false)) {
|
||||
prefs.edit().putBoolean(PREF_GAME_WAS_LAUNCHED, false).apply()
|
||||
gamesViewModel.resortGames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HomeScreen(
|
||||
gamesViewModel: GamesViewModel,
|
||||
onGameLaunch: (String) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
onNavigateToUserManagement: () -> Unit,
|
||||
onShowGameInfo: (Game) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val games by gamesViewModel.games.collectAsState()
|
||||
val isLoading by gamesViewModel.isReloading.collectAsState()
|
||||
val shouldScrollToTop by gamesViewModel.shouldScrollToTop.collectAsState()
|
||||
|
||||
val preferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||
val savedLayoutMode = remember {
|
||||
val savedValue = preferences.getString(PREF_LAYOUT_MODE, LayoutMode.CAROUSEL.name)
|
||||
try {
|
||||
LayoutMode.valueOf(savedValue ?: LayoutMode.CAROUSEL.name)
|
||||
} catch (_: Exception) {
|
||||
LayoutMode.CAROUSEL
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
(binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true)
|
||||
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout mode state (Grid or Carousel)
|
||||
var layoutMode by remember { mutableStateOf(savedLayoutMode) }
|
||||
var showLayoutDialog by remember { mutableStateOf(false) }
|
||||
var showSortDialog by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var isSearchExpanded by remember { mutableStateOf(false) }
|
||||
private var lastSearchText: String = ""
|
||||
private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
|
||||
|
||||
// Game properties screen state
|
||||
var selectedGameTile by remember { mutableStateOf<GameTile?>(null) }
|
||||
private fun setAdapter(games: List<Game>) {
|
||||
val currentSearchText = binding.searchText.text.toString()
|
||||
val currentFilter = binding.filterButton.id
|
||||
|
||||
// Sort mode state
|
||||
val savedSortMode = remember {
|
||||
val savedValue = preferences.getString(PREF_SORT_MODE, SortMode.LAST_PLAYED.name)
|
||||
try {
|
||||
SortMode.valueOf(savedValue ?: SortMode.LAST_PLAYED.name)
|
||||
} catch (_: Exception) {
|
||||
SortMode.LAST_PLAYED
|
||||
}
|
||||
}
|
||||
var sortMode by remember { mutableStateOf(savedSortMode) }
|
||||
val searchChanged = currentSearchText != lastSearchText
|
||||
val filterChanged = currentFilter != lastFilter
|
||||
|
||||
// Save sort mode when it changes
|
||||
LaunchedEffect(sortMode) {
|
||||
preferences.edit().putString(PREF_SORT_MODE, sortMode.name).apply()
|
||||
}
|
||||
|
||||
// Save layout mode when it changes
|
||||
LaunchedEffect(layoutMode) {
|
||||
preferences.edit().putString(PREF_LAYOUT_MODE, layoutMode.name).apply()
|
||||
}
|
||||
|
||||
// Remember last focused index across layout changes - load from preferences
|
||||
val savedFocusedIndex = remember {
|
||||
preferences.getInt(PREF_LAST_FOCUSED_INDEX, 0)
|
||||
}
|
||||
var lastFocusedIndex by remember { mutableStateOf(savedFocusedIndex) }
|
||||
|
||||
LaunchedEffect(lastFocusedIndex) {
|
||||
preferences.edit().putInt(PREF_LAST_FOCUSED_INDEX, lastFocusedIndex).apply()
|
||||
}
|
||||
|
||||
var listVersion by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(sortMode) {
|
||||
lastFocusedIndex = 0
|
||||
listVersion++
|
||||
}
|
||||
|
||||
// Reset to first item when games are resorted (e.g. after playing a game)
|
||||
// Else the sorting only takes effect after relaunch
|
||||
LaunchedEffect(shouldScrollToTop) {
|
||||
if (shouldScrollToTop) {
|
||||
lastFocusedIndex = 0
|
||||
listVersion++ // Force complete recomposition
|
||||
gamesViewModel.setShouldScrollToTop(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Load current user UUID
|
||||
var currentUserUuid by remember { mutableStateOf("") }
|
||||
LaunchedEffect(Unit) {
|
||||
currentUserUuid = NativeLibrary.getCurrentUser() ?: ""
|
||||
}
|
||||
|
||||
val headerFocusRequesters = remember {
|
||||
HeaderFocusRequesters(
|
||||
userButton = FocusRequester(),
|
||||
searchButton = FocusRequester(),
|
||||
sortButton = FocusRequester(),
|
||||
filterButton = FocusRequester(),
|
||||
settingsButton = FocusRequester(),
|
||||
)
|
||||
}
|
||||
// Don't recreate on layoutMode change - this causes zones state loss
|
||||
val contentFocusRequester = remember { FocusRequester() }
|
||||
|
||||
val navigationManager = remember {
|
||||
HomeNavigationManager(
|
||||
headerFocusRequesters = headerFocusRequesters,
|
||||
contentFocusRequester = contentFocusRequester,
|
||||
)
|
||||
}
|
||||
|
||||
// Request initial focus
|
||||
LaunchedEffect(Unit) {
|
||||
delay(200)
|
||||
navigationManager.requestInitialFocus()
|
||||
}
|
||||
|
||||
// Filter and sort games based on search query and sort mode
|
||||
val filteredGames = remember(games, searchQuery, sortMode) {
|
||||
val filtered = if (searchQuery.isBlank()) {
|
||||
games
|
||||
if (searchChanged || filterChanged) {
|
||||
filterAndSearch(games)
|
||||
lastSearchText = currentSearchText
|
||||
lastFilter = currentFilter
|
||||
} else {
|
||||
games.filter { game ->
|
||||
game.title.lowercase(Locale.getDefault())
|
||||
.contains(searchQuery.lowercase(Locale.getDefault()))
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
when (sortMode) {
|
||||
SortMode.LAST_PLAYED -> {
|
||||
filtered.sortedByDescending { game ->
|
||||
preferences.getLong(game.keyLastPlayedTime, 0L)
|
||||
}
|
||||
}
|
||||
|
||||
SortMode.ALPHABETICAL -> {
|
||||
filtered.sortedBy { it.title.lowercase(Locale.getDefault()) }
|
||||
}
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
|
||||
gamesViewModel.setFilteredGames(games)
|
||||
}
|
||||
}
|
||||
|
||||
val gameTiles = remember(filteredGames) {
|
||||
filteredGames.map { game ->
|
||||
GameTile.fromGame(game)
|
||||
private fun setupTopView() {
|
||||
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
|
||||
if (text.toString().isNotEmpty()) {
|
||||
binding.clearButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.clearButton.visibility = View.INVISIBLE
|
||||
}
|
||||
filterAndSearch()
|
||||
}
|
||||
|
||||
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
|
||||
binding.searchBackground.setOnClickListener { focusSearch() }
|
||||
|
||||
// Setup view button
|
||||
binding.viewButton.setOnClickListener { showViewMenu(it) }
|
||||
|
||||
// Setup filter button
|
||||
binding.filterButton.setOnClickListener { view ->
|
||||
showFilterMenu(view)
|
||||
}
|
||||
|
||||
// Setup settings button
|
||||
binding.settingsButton.setOnClickListener { navigateToSettings() }
|
||||
}
|
||||
|
||||
// NO key handling at top level
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
RetroGridBackground()
|
||||
private fun navigateToSettings() {
|
||||
val navController = findNavController()
|
||||
navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
|
||||
}
|
||||
|
||||
val contentOffset = 24.dp
|
||||
key(listVersion, layoutMode) {
|
||||
when (layoutMode) {
|
||||
LayoutMode.TWO_ROW -> {
|
||||
GameGrid(
|
||||
gameTiles = gameTiles,
|
||||
onGameClick = { tile: GameTile ->
|
||||
val path = tile.uri ?: return@GameGrid
|
||||
onGameLaunch(path)
|
||||
},
|
||||
onGameLongClick = { tile: GameTile ->
|
||||
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||
if (game != null) {
|
||||
onShowGameInfo(game)
|
||||
}
|
||||
},
|
||||
onNavigateToSettings = {},
|
||||
focusRequester = contentFocusRequester,
|
||||
onNavigateUp = {
|
||||
navigationManager.navigateUp()
|
||||
},
|
||||
rowCount = 2,
|
||||
tileIconSize = 120.dp,
|
||||
isLoading = isLoading,
|
||||
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
|
||||
initialFocusedIndex = lastFocusedIndex,
|
||||
onFocusedIndexChanged = { newIndex ->
|
||||
lastFocusedIndex = newIndex
|
||||
},
|
||||
onShowGameInfo = { tile: GameTile ->
|
||||
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||
if (game != null) {
|
||||
onShowGameInfo(game)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = contentOffset),
|
||||
)
|
||||
private fun showViewMenu(anchor: View) {
|
||||
val popup = PopupMenu(requireContext(), anchor)
|
||||
popup.menuInflater.inflate(R.menu.menu_game_views, popup.menu)
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
if (!isLandscape) {
|
||||
popup.menu.findItem(R.id.view_carousel)?.isVisible = false
|
||||
}
|
||||
|
||||
val currentViewType = getCurrentViewType()
|
||||
when (currentViewType) {
|
||||
GameAdapter.VIEW_TYPE_LIST -> popup.menu.findItem(R.id.view_list).isChecked = true
|
||||
GameAdapter.VIEW_TYPE_GRID_COMPACT -> popup.menu.findItem(R.id.view_grid_compact).isChecked = true
|
||||
GameAdapter.VIEW_TYPE_GRID -> popup.menu.findItem(R.id.view_grid).isChecked = true
|
||||
GameAdapter.VIEW_TYPE_CAROUSEL -> popup.menu.findItem(R.id.view_carousel).isChecked = true
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.view_grid -> {
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
true
|
||||
}
|
||||
|
||||
LayoutMode.CAROUSEL -> {
|
||||
GameCarousel(
|
||||
gameTiles = gameTiles,
|
||||
onGameClick = { tile: GameTile ->
|
||||
val path = tile.uri ?: return@GameCarousel
|
||||
onGameLaunch(path)
|
||||
},
|
||||
onGameLongClick = { tile: GameTile ->
|
||||
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||
if (game != null) {
|
||||
onShowGameInfo(game)
|
||||
}
|
||||
},
|
||||
focusRequester = contentFocusRequester,
|
||||
onNavigateUp = {
|
||||
navigationManager.navigateUp()
|
||||
},
|
||||
initialFocusedIndex = lastFocusedIndex,
|
||||
onFocusedIndexChanged = { newIndex ->
|
||||
lastFocusedIndex = newIndex
|
||||
},
|
||||
onShowGameInfo = { tile: GameTile ->
|
||||
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||
if (game != null) {
|
||||
onShowGameInfo(game)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = contentOffset),
|
||||
)
|
||||
R.id.view_grid_compact -> {
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID_COMPACT)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Default to Grid
|
||||
GameGrid(
|
||||
gameTiles = gameTiles,
|
||||
onGameClick = { tile: GameTile ->
|
||||
val path = tile.uri ?: return@GameGrid
|
||||
onGameLaunch(path)
|
||||
},
|
||||
onNavigateToSettings = {},
|
||||
focusRequester = contentFocusRequester,
|
||||
onNavigateUp = {
|
||||
navigationManager.navigateUp()
|
||||
},
|
||||
rowCount = 2,
|
||||
tileIconSize = 120.dp,
|
||||
isLoading = isLoading,
|
||||
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = contentOffset),
|
||||
)
|
||||
R.id.view_list -> {
|
||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_LIST)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Refocus content when layout mode changes
|
||||
LaunchedEffect(layoutMode) {
|
||||
delay(200)
|
||||
contentFocusRequester.requestFocus()
|
||||
navigationManager.resetToContent()
|
||||
}
|
||||
|
||||
// Refocus content when sort mode changes
|
||||
LaunchedEffect(sortMode) {
|
||||
delay(200)
|
||||
contentFocusRequester.requestFocus()
|
||||
navigationManager.resetToContent()
|
||||
}
|
||||
|
||||
// Header
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (navigationManager.currentZone == NavigationZone.HEADER) {
|
||||
handleHomeNavigation(keyEvent, navigationManager)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
R.id.view_carousel -> {
|
||||
if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) {
|
||||
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
|
||||
applyGridGamesBinding()
|
||||
item.isChecked = true
|
||||
onResume()
|
||||
}
|
||||
) {
|
||||
HomeHeader(
|
||||
currentUser = currentUserUuid,
|
||||
onUserClick = onNavigateToUserManagement,
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChange = { query -> searchQuery = query },
|
||||
isSearchExpanded = isSearchExpanded,
|
||||
onSearchExpandedChange = { expanded -> isSearchExpanded = expanded },
|
||||
onSortClick = {
|
||||
showSortDialog = true
|
||||
},
|
||||
onFilterClick = {
|
||||
showLayoutDialog = true
|
||||
},
|
||||
onSettingsClick = onNavigateToSettings,
|
||||
focusRequesters = headerFocusRequesters,
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
val currentZone by navigationManager.currentZoneState
|
||||
val headerIdx by navigationManager.headerIndexState
|
||||
val footerActions = when (currentZone) {
|
||||
NavigationZone.HEADER -> {
|
||||
val headerText = when (headerIdx) {
|
||||
0 -> context.getString(R.string.footer_open) // User profile
|
||||
1 -> context.getString(R.string.home_search) // Search
|
||||
else -> context.getString(R.string.footer_open) // Sort, Filter, Settings
|
||||
}
|
||||
listOf(FooterAction(FooterButton.A, headerText))
|
||||
}
|
||||
NavigationZone.CONTENT -> {
|
||||
listOf(
|
||||
FooterAction(FooterButton.A, context.getString(R.string.home_start)),
|
||||
FooterAction(FooterButton.X, context.getString(R.string.footer_game_info)),
|
||||
)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
HomeFooter(
|
||||
actions = footerActions,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
|
||||
// Layout selection dialog
|
||||
if (showLayoutDialog) {
|
||||
LayoutSelectionDialog(
|
||||
onDismiss = { showLayoutDialog = false },
|
||||
onSelectGrid = { layoutMode = LayoutMode.TWO_ROW },
|
||||
onSelectCarousel = { layoutMode = LayoutMode.CAROUSEL },
|
||||
)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showFilterMenu(anchor: View) {
|
||||
val popup = PopupMenu(requireContext(), anchor)
|
||||
popup.menuInflater.inflate(R.menu.menu_game_filters, popup.menu)
|
||||
|
||||
// Set checked state based on current filter
|
||||
when (currentFilter) {
|
||||
R.id.alphabetical -> popup.menu.findItem(R.id.alphabetical).isChecked = true
|
||||
R.id.filter_recently_played -> popup.menu.findItem(R.id.filter_recently_played).isChecked =
|
||||
true
|
||||
|
||||
R.id.filter_recently_added -> popup.menu.findItem(R.id.filter_recently_added).isChecked =
|
||||
true
|
||||
}
|
||||
|
||||
// Sort selection dialog
|
||||
if (showSortDialog) {
|
||||
SortSelectionDialog(
|
||||
currentSortMode = sortMode,
|
||||
onDismiss = { showSortDialog = false },
|
||||
onSelectSort = { selectedSortMode ->
|
||||
sortMode = selectedSortMode
|
||||
gamesViewModel.resortGames()
|
||||
},
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
currentFilter = item.itemId
|
||||
preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) }
|
||||
filterAndSearch()
|
||||
true
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
// Track current filter
|
||||
private var currentFilter = View.NO_ID
|
||||
|
||||
private fun filterAndSearch(baseList: List<Game> = gamesViewModel.games.value) {
|
||||
val filteredList: List<Game> = when (currentFilter) {
|
||||
R.id.alphabetical -> baseList.sortedBy { it.title }
|
||||
R.id.filter_recently_played -> {
|
||||
baseList.filter {
|
||||
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
|
||||
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
||||
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
|
||||
}
|
||||
R.id.filter_recently_added -> {
|
||||
baseList.filter {
|
||||
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
|
||||
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
||||
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
|
||||
}
|
||||
else -> baseList
|
||||
}
|
||||
|
||||
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
||||
if (searchTerm.isEmpty()) {
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
|
||||
filteredList
|
||||
)
|
||||
gamesViewModel.setFilteredGames(filteredList)
|
||||
return
|
||||
}
|
||||
|
||||
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
|
||||
val sortedList = filteredList.mapNotNull { game ->
|
||||
val title = game.title.lowercase(Locale.getDefault())
|
||||
val score = searchAlgorithm.similarity(searchTerm, title)
|
||||
if (score > 0.03) {
|
||||
ScoredGame(score, game)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedByDescending { it.score }.map { it.item }
|
||||
|
||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
|
||||
gamesViewModel.setFilteredGames(sortedList)
|
||||
}
|
||||
|
||||
private inner class ScoredGame(val score: Double, val item: Game)
|
||||
|
||||
private fun focusSearch() {
|
||||
binding.searchText.requestFocus()
|
||||
val imm = requireActivity()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
if (_binding != null) {
|
||||
(binding.gridGames as? CarouselRecyclerView)?.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchQLaunch() {
|
||||
try {
|
||||
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.QLaunch.entryId)
|
||||
if (appletPath.isEmpty()) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.applets_error_applet,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
NativeLibrary.setCurrentAppletId(AppletInfo.QLaunch.appletId)
|
||||
|
||||
val qlaunchGame = Game(
|
||||
title = getString(R.string.qlaunch_applet),
|
||||
path = appletPath
|
||||
)
|
||||
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(qlaunchGame)
|
||||
findNavController().navigate(action)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Failed to launch QLaunch: ${e.message}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateButtonsVisibility() {
|
||||
val showQLaunch = BooleanSetting.ENABLE_QLAUNCH_BUTTON.getBoolean()
|
||||
val showFolder = BooleanSetting.ENABLE_FOLDER_BUTTON.getBoolean()
|
||||
val isFirmwareAvailable = NativeLibrary.isFirmwareAvailable()
|
||||
|
||||
val shouldShowQLaunch = showQLaunch && isFirmwareAvailable
|
||||
binding.launchQlaunch.visibility = if (shouldShowQLaunch) View.VISIBLE else View.GONE
|
||||
|
||||
binding.addDirectory.visibility = if (showFolder) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun setInsets() =
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
||||
|
||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressViewEndTarget(
|
||||
false,
|
||||
barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
|
||||
)
|
||||
|
||||
val leftInset = barInsets.left + cutoutInsets.left
|
||||
val rightInset = barInsets.right + cutoutInsets.right
|
||||
val topInset = maxOf(barInsets.top, cutoutInsets.top)
|
||||
|
||||
val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
|
||||
mlpSwipe.leftMargin = leftInset
|
||||
mlpSwipe.rightMargin = rightInset
|
||||
binding.swipeRefresh.layoutParams = mlpSwipe
|
||||
|
||||
val mlpHeader = binding.header.layoutParams as ViewGroup.MarginLayoutParams
|
||||
|
||||
// Store original margins only once
|
||||
if (originalHeaderTopMargin == null) {
|
||||
originalHeaderTopMargin = mlpHeader.topMargin
|
||||
originalHeaderRightMargin = mlpHeader.rightMargin
|
||||
originalHeaderLeftMargin = mlpHeader.leftMargin
|
||||
}
|
||||
|
||||
// Always set margin as original + insets
|
||||
mlpHeader.leftMargin = (originalHeaderLeftMargin ?: 0) + leftInset
|
||||
mlpHeader.rightMargin = (originalHeaderRightMargin ?: 0) + rightInset
|
||||
mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize(
|
||||
R.dimen.spacing_med
|
||||
)
|
||||
binding.header.layoutParams = mlpHeader
|
||||
|
||||
binding.noticeText.updatePadding(bottom = spacingNavigation)
|
||||
|
||||
binding.gridGames.updatePadding(
|
||||
top = resources.getDimensionPixelSize(R.dimen.spacing_med)
|
||||
)
|
||||
|
||||
val mlpFab = binding.addDirectory.layoutParams as ViewGroup.MarginLayoutParams
|
||||
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
||||
mlpFab.leftMargin = leftInset + fabPadding
|
||||
mlpFab.bottomMargin = barInsets.bottom + fabPadding
|
||||
mlpFab.rightMargin = rightInset + fabPadding
|
||||
binding.addDirectory.layoutParams = mlpFab
|
||||
|
||||
binding.launchQlaunch?.let { qlaunchButton ->
|
||||
val mlpQLaunch = qlaunchButton.layoutParams as ViewGroup.MarginLayoutParams
|
||||
mlpQLaunch.leftMargin = leftInset + fabPadding
|
||||
mlpQLaunch.bottomMargin = barInsets.bottom + fabPadding
|
||||
qlaunchButton.layoutParams = mlpQLaunch
|
||||
}
|
||||
|
||||
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
|
||||
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
|
||||
fallbackBottomInset = bottomInset
|
||||
(binding.gridGames as? CarouselRecyclerView)?.notifyInsetsReady(bottomInset)
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +124,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||
|
||||
// Hide status bar and navigation bar for fullscreen experience
|
||||
// Maybe a setting? Reminds me: SafeArea Testing!
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
hide(WindowInsetsCompat.Type.statusBars())
|
||||
hide(WindowInsetsCompat.Type.navigationBars())
|
||||
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
window.statusBarColor =
|
||||
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
||||
window.navigationBarColor =
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -28,29 +25,31 @@ import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
|
||||
class GameObjectIconFetcher(
|
||||
class GameIconFetcher(
|
||||
private val game: Game,
|
||||
private val options: Options
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
val bitmap = decodeGameIcon(game.path)
|
||||
?: throw IllegalStateException("Failed to decode game icon for: ${game.title}")
|
||||
|
||||
return DrawableResult(
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeGameIcon(path: String): Bitmap? {
|
||||
val iconBytes = GameMetadata.getIcon(path)
|
||||
return BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
|
||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
||||
val data = GameMetadata.getIcon(uri)
|
||||
return BitmapFactory.decodeByteArray(
|
||||
data,
|
||||
0,
|
||||
data.size,
|
||||
BitmapFactory.Options()
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Game> {
|
||||
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
|
||||
GameObjectIconFetcher(data, options)
|
||||
GameIconFetcher(data, options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ object GameIconUtils {
|
||||
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
|
||||
.components {
|
||||
add(GameIconKeyer())
|
||||
add(GameObjectIconFetcher.Factory())
|
||||
add(GameIconFetcher.Factory())
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(YuzuApplication.appContext)
|
||||
@@ -100,6 +99,9 @@ object GameIconUtils {
|
||||
R.id.shortcut_foreground,
|
||||
getGameIcon(lifecycleOwner, game).toDrawable(YuzuApplication.appContext.resources)
|
||||
)
|
||||
val inset = YuzuApplication.appContext.resources
|
||||
.getDimensionPixelSize(R.dimen.icon_inset)
|
||||
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
|
||||
return IconCompat.createWithAdaptiveBitmap(
|
||||
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
@@ -74,7 +74,7 @@ class GradientBorderCardView @JvmOverloads constructor(
|
||||
borderPaint.shader = null
|
||||
val typedValue = android.util.TypedValue()
|
||||
context.theme.resolveAttribute(
|
||||
androidx.appcompat.R.attr.colorPrimary,
|
||||
com.google.android.material.R.attr.colorPrimary,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
|
||||
@@ -1233,6 +1233,39 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobje
|
||||
return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
|
||||
if (enabled) {
|
||||
Settings::values.use_speed_limit.SetValue(true);
|
||||
Settings::SetSpeedMode(Settings::SpeedMode::Turbo);
|
||||
} else {
|
||||
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
|
||||
}
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setSlowSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
|
||||
if (enabled) {
|
||||
Settings::values.use_speed_limit.SetValue(true);
|
||||
Settings::SetSpeedMode(Settings::SpeedMode::Slow);
|
||||
} else {
|
||||
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
|
||||
}
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setStandardSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
|
||||
Settings::values.use_speed_limit.SetValue(enabled);
|
||||
if (enabled) {
|
||||
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
|
||||
}
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isTurboMode(JNIEnv *env, jobject jobj) {
|
||||
return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Turbo;
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isSlowMode(JNIEnv *env, jobject jobj) {
|
||||
return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Slow;
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run(JNIEnv* env, jobject jobj, jstring j_path,
|
||||
jint j_program_index,
|
||||
jboolean j_frontend_initiated) {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zm0,7h12v-2H3v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switch_enable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/gpu_unswizzle_enable" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_texture_size"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/gpu_unswizzle_texture_size">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/dropdown_texture_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_stream_size"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/gpu_unswizzle_stream_size">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/dropdown_stream_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_chunk_size"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/gpu_unswizzle_chunk_size">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/dropdown_chunk_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:spacing="8dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_default"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/gpu_unswizzle_default_button" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_cancel"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/cancel" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_ok"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@android:string/ok" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -8,7 +8,7 @@
|
||||
<fragment
|
||||
android:id="@+id/gamesFragment"
|
||||
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
|
||||
android:label="PlatformGamesFragment">
|
||||
android:label="PlatformGamesFragment" />
|
||||
<action
|
||||
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
|
||||
app:destination="@id/homeSettingsFragment"
|
||||
@@ -16,14 +16,6 @@
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_gamesFragment_to_profileManagerFragment"
|
||||
app:destination="@id/profileManagerFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/homeSettingsFragment"
|
||||
@@ -166,9 +158,6 @@
|
||||
<action
|
||||
android:id="@+id/action_global_perGamePropertiesFragment"
|
||||
app:destination="@id/perGamePropertiesFragment" />
|
||||
<action
|
||||
android:id="@+id/action_global_gameInfoFragment"
|
||||
app:destination="@id/gameInfoFragment" />
|
||||
<fragment
|
||||
android:id="@+id/gameInfoFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
|
||||
|
||||
@@ -229,10 +229,6 @@
|
||||
<string name="update_install_failed">Installieren des Updates fehlgeschlagen: %1$s</string>
|
||||
<string name="home_search">Suche</string>
|
||||
<string name="home_settings">Einstellungen</string>
|
||||
<string name="home_sort">Sortieren</string>
|
||||
<string name="home_layout">Layout</string>
|
||||
<string name="home_start">Start</string>
|
||||
<string name="home_clear">Löschen</string>
|
||||
<string name="empty_gamelist">Es wurden keine Dateien gefunden oder es wurde noch kein Spielverzeichnis ausgewählt.</string>
|
||||
<string name="manage_game_folders">Spiele-Ordner verwalten</string>
|
||||
<string name="select_games_folder_description">Erlaubt Eden die Spieleliste zu füllen</string>
|
||||
@@ -709,11 +705,6 @@ Wird der Handheld-Modus verwendet, verringert es die Auflösung und erhöht die
|
||||
<string name="path_set">Pfad erfolgreich gesetzt</string>
|
||||
<string name="skip_migration">Überspringen</string>
|
||||
|
||||
<!-- Footer actions -->
|
||||
<string name="footer_open">Öffnen</string>
|
||||
<string name="footer_game_info">Spielinfo</string>
|
||||
<string name="footer_back">Zurück</string>
|
||||
|
||||
<!-- Game properties -->
|
||||
<string name="info">Info</string>
|
||||
<string name="info_description">Programm-ID, Entwickler, Version</string>
|
||||
|
||||
@@ -530,6 +530,21 @@
|
||||
<item>2</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="framePacingModeNames">
|
||||
<item>@string/frame_pacing_mode_target_Auto</item>
|
||||
<item>@string/frame_pacing_mode_target_30</item>
|
||||
<item>@string/frame_pacing_mode_target_60</item>
|
||||
<item>@string/frame_pacing_mode_target_90</item>
|
||||
<item>@string/frame_pacing_mode_target_120</item>
|
||||
</string-array>
|
||||
<integer-array name="framePacingModeValues">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="appletEntries">
|
||||
<item>@string/applet_hle</item>
|
||||
<item>@string/applet_lle</item>
|
||||
|
||||
@@ -240,10 +240,6 @@
|
||||
<string name="update_install_failed">Failed to install update: %1$s</string>
|
||||
<string name="home_search">Search</string>
|
||||
<string name="home_settings">Settings</string>
|
||||
<string name="home_sort">Sort</string>
|
||||
<string name="home_layout">Layout</string>
|
||||
<string name="home_start">Start</string>
|
||||
<string name="home_clear">Clear</string>
|
||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||
<string name="manage_game_folders">Manage game folders</string>
|
||||
<string name="select_games_folder_description">Allows Eden to populate the games list</string>
|
||||
@@ -426,6 +422,10 @@
|
||||
<string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
|
||||
<string name="frame_limit_slider">Limit speed percent</string>
|
||||
<string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
|
||||
<string name="turbo_speed_limit">Turbo speed</string>
|
||||
<string name="turbo_speed_limit_description">When Turbo Mode is enabled, emulation will run at this speed.</string>
|
||||
<string name="slow_speed_limit">Slow speed</string>
|
||||
<string name="slow_speed_limit_description">When Slow Mode is enabled, emulation will run at this speed.</string>
|
||||
<string name="cpu_backend">CPU backend</string>
|
||||
<string name="cpu_accuracy">CPU accuracy</string>
|
||||
<string name="value_with_units">%1$s%2$s</string>
|
||||
@@ -477,6 +477,8 @@
|
||||
<string name="renderer_accuracy_description">Controls the GPU emulation mode. Most games render fine with Fast or Balanced modes, but Accurate is still required for some. Particles tend to only render correctly with Accurate mode.</string>
|
||||
<string name="dma_accuracy">DMA Accuracy</string>
|
||||
<string name="dma_accuracy_description">Controls the DMA precision accuracy. Safe precision can fix issues in some games, but it can also impact performance in some cases. If unsure, leave this on Default.</string>
|
||||
<string name="frame_pacing_mode">Frame Pacing Mode</string>
|
||||
<string name="frame_pacing_mode_description">Controls how the emulator manages frame pacing to reduce stuttering and make the frame rate smoother and more consistent.</string>
|
||||
<string name="anisotropic_filtering">Anisotropic filtering</string>
|
||||
<string name="anisotropic_filtering_description">Improves the quality of textures when viewed at oblique angles</string>
|
||||
<string name="vram_usage_mode">VRAM Usage Mode</string>
|
||||
@@ -505,15 +507,20 @@
|
||||
<string name="skip_cpu_inner_invalidation">Skip CPU Inner Invalidation</string>
|
||||
<string name="skip_cpu_inner_invalidation_description">Skips certain CPU-side cache invalidations during memory updates, reducing CPU usage and improving it\'s performance. This may cause glitches or crashes on some games.</string>
|
||||
<string name="fix_bloom_effects">Fix Bloom Effects</string>
|
||||
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno 700), removes bloom in Burnout</string>
|
||||
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno 700), removes bloom in Burnout. Warning: may cause graphical artifacts in other games.</string>
|
||||
<string name="renderer_asynchronous_shaders">Use asynchronous shaders</string>
|
||||
<string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously. This may reduce stutters but may also introduce glitches.</string>
|
||||
<string name="gpu_unswizzle_settings">GPU Unswizzle Settings</string>
|
||||
<string name="gpu_unswizzle_settings_description">Configure GPU-based texture unswizzling parameters or disable it entirely. Adjust these settings to balance performance and texture loading quality.</string>
|
||||
<string name="gpu_unswizzle_enable">Enable GPU Unswizzle</string>
|
||||
<string name="gpu_unswizzle_disabled">Disabled</string>
|
||||
<string name="gpu_unswizzle_texture_size">GPU Unswizzle Max Texture Size</string>
|
||||
<string name="gpu_unswizzle_texture_size_description">Sets the maximum size (MB) for GPU-based texture unswizzling. While the GPU is faster for medium and large textures, the CPU may be more efficient for very small ones. Adjust this to find the balance between GPU acceleration and CPU overhead.</string>
|
||||
<string name="gpu_unswizzle_stream_size">GPU Unswizzle Stream Size</string>
|
||||
<string name="gpu_unswizzle_stream_size_description">Sets the data limit per frame for unswizzling large textures. Higher values speed up texture loading at the cost of higher frame latency; lower values reduce GPU overhead but may cause visible texture pop-in.</string>
|
||||
<string name="gpu_unswizzle_chunk_size">GPU Unswizzle Chunk Size</string>
|
||||
<string name="gpu_unswizzle_chunk_size_description">Defines the number of depth slices processed per batch for 3D textures. Increasing this improves throughput efficiency on powerful GPUs but may cause stuttering or driver timeouts on weaker hardware.</string>
|
||||
<string name="gpu_unswizzle_default_button">Default</string>
|
||||
|
||||
|
||||
<string name="extensions">Extensions</string>
|
||||
@@ -770,11 +777,6 @@
|
||||
<string name="path_set">Path set successfully</string>
|
||||
<string name="skip_migration">Skip</string>
|
||||
|
||||
<!-- Footer actions -->
|
||||
<string name="footer_open">Open</string>
|
||||
<string name="footer_game_info">Game Info</string>
|
||||
<string name="footer_back">Back</string>
|
||||
|
||||
<!-- Game properties -->
|
||||
<string name="info">Info</string>
|
||||
<string name="info_description">Program ID, developer, version</string>
|
||||
@@ -1032,6 +1034,13 @@
|
||||
<string name="dma_accuracy_unsafe">Unsafe</string>
|
||||
<string name="dma_accuracy_safe">Safe</string>
|
||||
|
||||
<!-- Frame Pacing Mode -->
|
||||
<string name="frame_pacing_mode_target_Auto">Auto</string>
|
||||
<string name="frame_pacing_mode_target_30">30 FPS</string>
|
||||
<string name="frame_pacing_mode_target_60">60 FPS</string>
|
||||
<string name="frame_pacing_mode_target_90">90 FPS</string>
|
||||
<string name="frame_pacing_mode_target_120">120 FPS</string>
|
||||
|
||||
<!-- ASTC Decoding Method Choices -->
|
||||
<string name="accelerate_astc_cpu" translatable="false">CPU</string>
|
||||
<string name="accelerate_astc_gpu" translatable="false">GPU</string>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
@@ -8,8 +5,7 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("com.android.library") version "8.1.4" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
|
||||
@@ -186,6 +186,8 @@ if(ARCHITECTURE_x86_64)
|
||||
common
|
||||
PRIVATE x64/cpu_detect.cpp
|
||||
x64/cpu_detect.h
|
||||
x64/cpu_wait.cpp
|
||||
x64/cpu_wait.h
|
||||
x64/native_clock.cpp
|
||||
x64/native_clock.h
|
||||
x64/rdtsc.cpp
|
||||
|
||||
@@ -219,11 +219,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
bool ClearBackingRegion(size_t physical_offset, size_t length) {
|
||||
// TODO: This does not seem to be possible on Windows.
|
||||
return false;
|
||||
}
|
||||
|
||||
void EnableDirectMappedAddress() {
|
||||
// TODO
|
||||
UNREACHABLE();
|
||||
@@ -617,24 +612,6 @@ public:
|
||||
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
|
||||
}
|
||||
|
||||
bool ClearBackingRegion(size_t physical_offset, size_t length) {
|
||||
#ifdef __linux__
|
||||
// Only incur syscall cost IF memset would be slower (theshold = 16MiB)
|
||||
// TODO(lizzie): Smarter way to dynamically get this threshold (broadwell != raptor lake) for example
|
||||
if (length >= 2097152UL * 8) {
|
||||
// Set MADV_REMOVE on backing map to destroy it instantly.
|
||||
// This also deletes the area from the backing file.
|
||||
int ret = madvise(backing_base + physical_offset, length, MADV_REMOVE);
|
||||
ASSERT_MSG(ret == 0, "madvise failed: {}", strerror(errno));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void EnableDirectMappedAddress() {
|
||||
virtual_base = nullptr;
|
||||
}
|
||||
@@ -748,9 +725,7 @@ void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission
|
||||
}
|
||||
|
||||
void HostMemory::ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value) {
|
||||
if (!impl || fill_value != 0 || !impl->ClearBackingRegion(physical_offset, length)) {
|
||||
std::memset(backing_base + physical_offset, fill_value, length);
|
||||
}
|
||||
std::memset(backing_base + physical_offset, fill_value, length);
|
||||
}
|
||||
|
||||
void HostMemory::EnableDirectMappedAddress() {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define COMPILER_ID "@CXX_COMPILER@"
|
||||
#define BUILD_AUTO_UPDATE_WEBSITE "@BUILD_AUTO_UPDATE_WEBSITE@"
|
||||
#define BUILD_AUTO_UPDATE_API "@BUILD_AUTO_UPDATE_API@"
|
||||
#define BUILD_AUTO_UPDATE_API_PATH "@BUILD_AUTO_UPDATE_API_PATH@"
|
||||
#define BUILD_AUTO_UPDATE_REPO "@BUILD_AUTO_UPDATE_REPO@"
|
||||
#define IS_NIGHTLY_BUILD @IS_NIGHTLY_BUILD@
|
||||
|
||||
@@ -42,6 +43,7 @@ constexpr const bool g_is_nightly_build = IS_NIGHTLY_BUILD;
|
||||
|
||||
constexpr const char g_build_auto_update_website[] = BUILD_AUTO_UPDATE_WEBSITE;
|
||||
constexpr const char g_build_auto_update_api[] = BUILD_AUTO_UPDATE_API;
|
||||
constexpr const char g_build_auto_update_api_path[] = BUILD_AUTO_UPDATE_API_PATH;
|
||||
constexpr const char g_build_auto_update_repo[] = BUILD_AUTO_UPDATE_REPO;
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -26,6 +26,7 @@ extern const bool g_is_nightly_build;
|
||||
|
||||
extern const char g_build_auto_update_website[];
|
||||
extern const char g_build_auto_update_api[];
|
||||
extern const char g_build_auto_update_api_path[];
|
||||
extern const char g_build_auto_update_repo[];
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -379,4 +379,52 @@ void SetConfiguringGlobal(bool is_global) {
|
||||
configuring_global = is_global;
|
||||
}
|
||||
|
||||
u16 SpeedLimit() {
|
||||
switch (SpeedMode(values.current_speed_mode)) {
|
||||
case SpeedMode::Standard:
|
||||
return values.speed_limit.GetValue();
|
||||
case SpeedMode::Turbo:
|
||||
return values.turbo_speed_limit.GetValue();
|
||||
case SpeedMode::Slow:
|
||||
return values.slow_speed_limit.GetValue();
|
||||
default:
|
||||
UNIMPLEMENTED();
|
||||
}
|
||||
|
||||
return 100;
|
||||
}
|
||||
|
||||
void SetSpeedMode(const SpeedMode& mode) {
|
||||
values.current_speed_mode.SetValue(mode);
|
||||
|
||||
switch (mode) {
|
||||
case SpeedMode::Turbo:
|
||||
case SpeedMode::Slow:
|
||||
values.use_speed_limit.SetValue(true);
|
||||
break;
|
||||
case SpeedMode::Standard:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleStandardMode() {
|
||||
values.use_speed_limit.SetValue(!values.use_speed_limit.GetValue());
|
||||
SetSpeedMode(SpeedMode::Standard);
|
||||
}
|
||||
|
||||
void ToggleTurboMode() {
|
||||
if (values.current_speed_mode.GetValue() != SpeedMode::Turbo)
|
||||
SetSpeedMode(SpeedMode::Turbo);
|
||||
else
|
||||
SetSpeedMode(SpeedMode::Standard);
|
||||
}
|
||||
|
||||
void ToggleSlowMode() {
|
||||
if (values.current_speed_mode.GetValue() != SpeedMode::Slow)
|
||||
SetSpeedMode(SpeedMode::Slow);
|
||||
else
|
||||
SetSpeedMode(SpeedMode::Standard);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
|
||||
+50
-13
@@ -204,6 +204,7 @@ struct Values {
|
||||
true};
|
||||
SwitchableSetting<bool> use_speed_limit{
|
||||
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, true, true};
|
||||
|
||||
SwitchableSetting<u16, true> speed_limit{linkage,
|
||||
100,
|
||||
0,
|
||||
@@ -214,6 +215,30 @@ struct Values {
|
||||
true,
|
||||
true,
|
||||
&use_speed_limit};
|
||||
|
||||
SwitchableSetting<u16, true> slow_speed_limit{linkage,
|
||||
50,
|
||||
0,
|
||||
9999,
|
||||
"slow_speed_limit",
|
||||
Category::Core,
|
||||
Specialization::Countable | Specialization::Percentage,
|
||||
true,
|
||||
true};
|
||||
|
||||
SwitchableSetting<u16, true> turbo_speed_limit{linkage,
|
||||
200,
|
||||
0,
|
||||
9999,
|
||||
"turbo_speed_limit",
|
||||
Category::Core,
|
||||
Specialization::Countable | Specialization::Percentage,
|
||||
true,
|
||||
true};
|
||||
|
||||
// The currently used speed mode.
|
||||
Setting<SpeedMode> current_speed_mode{linkage, SpeedMode::Standard, "current_speed_mode", Category::Core, Specialization::Default, false, true};
|
||||
|
||||
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core,
|
||||
Specialization::Default};
|
||||
|
||||
@@ -434,6 +459,16 @@ struct Values {
|
||||
"accelerate_astc",
|
||||
Category::RendererAdvanced};
|
||||
|
||||
SwitchableSetting<FramePacingMode, true> frame_pacing_mode{linkage,
|
||||
FramePacingMode::Target_Auto,
|
||||
FramePacingMode::Target_Auto,
|
||||
FramePacingMode::Target_120,
|
||||
"frame_pacing_mode",
|
||||
Category::RendererAdvanced,
|
||||
Specialization::Default,
|
||||
true,
|
||||
true};
|
||||
|
||||
SwitchableSetting<AstcRecompression, true> astc_recompression{linkage,
|
||||
AstcRecompression::Uncompressed,
|
||||
"astc_recompression",
|
||||
@@ -532,14 +567,11 @@ struct Values {
|
||||
Category::RendererHacks,
|
||||
Specialization::Default};
|
||||
|
||||
SwitchableSetting<bool> gpu_unswizzle_enabled{linkage, false, "gpu_unswizzle_enabled",
|
||||
Category::RendererHacks};
|
||||
|
||||
SwitchableSetting<ExtendedDynamicState> dyna_state{linkage,
|
||||
#if defined (_WIN32)
|
||||
ExtendedDynamicState::EDS3,
|
||||
#elif defined (__FreeBSD__)
|
||||
ExtendedDynamicState::EDS3,
|
||||
#elif defined (ANDROID)
|
||||
ExtendedDynamicState::Disabled,
|
||||
#elif defined (__APPLE__)
|
||||
#if defined (ANDROID) || defined (__APPLE__)
|
||||
ExtendedDynamicState::Disabled,
|
||||
#else
|
||||
ExtendedDynamicState::EDS2,
|
||||
@@ -591,11 +623,11 @@ struct Values {
|
||||
"language_index",
|
||||
Category::System};
|
||||
SwitchableSetting<Region, true> region_index{linkage, Region::Usa, "region_index", Category::System};
|
||||
SwitchableSetting<TimeZone, true> time_zone_index{linkage, TimeZone::Auto,
|
||||
"time_zone_index", Category::System};
|
||||
SwitchableSetting<TimeZone, true> time_zone_index{linkage, TimeZone::Auto, "time_zone_index", Category::System};
|
||||
Setting<u32> serial_battery{linkage, 0, "serial_battery", Category::System};
|
||||
Setting<u32> serial_unit{linkage, 0, "serial_unit", Category::System};
|
||||
// Measured in seconds since epoch
|
||||
SwitchableSetting<bool> custom_rtc_enabled{
|
||||
linkage, false, "custom_rtc_enabled", Category::System, Specialization::Paired, true, true};
|
||||
SwitchableSetting<bool> custom_rtc_enabled{linkage, false, "custom_rtc_enabled", Category::System, Specialization::Paired, true, true};
|
||||
SwitchableSetting<s64> custom_rtc{
|
||||
linkage, 0, "custom_rtc", Category::System, Specialization::Time,
|
||||
false, true, &custom_rtc_enabled};
|
||||
@@ -765,8 +797,6 @@ struct Values {
|
||||
true};
|
||||
|
||||
// Miscellaneous
|
||||
Setting<std::string> serial_battery{linkage, std::string(), "serial_battery", Category::Miscellaneous};
|
||||
Setting<std::string> serial_unit{linkage, std::string(), "serial_unit", Category::Miscellaneous};
|
||||
Setting<std::string> log_filter{linkage, "*:Info", "log_filter", Category::Miscellaneous};
|
||||
Setting<bool> log_flush_line{linkage, false, "flush_line", Category::Miscellaneous, Specialization::Default, true, true};
|
||||
Setting<bool> censor_username{linkage, true, "censor_username", Category::Miscellaneous};
|
||||
@@ -814,6 +844,13 @@ bool IsDockedMode();
|
||||
|
||||
float Volume();
|
||||
|
||||
// speed limit ops
|
||||
u16 SpeedLimit();
|
||||
void SetSpeedMode(const SpeedMode &mode);
|
||||
void ToggleStandardMode();
|
||||
void ToggleTurboMode();
|
||||
void ToggleSlowMode();
|
||||
|
||||
std::string GetTimeZoneString(TimeZone time_zone);
|
||||
|
||||
void LogSettings();
|
||||
|
||||
@@ -129,6 +129,7 @@ ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt
|
||||
ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16, X32, X64, None);
|
||||
ENUM(AstcDecodeMode, Cpu, Gpu, CpuAsynchronous);
|
||||
ENUM(AstcRecompression, Uncompressed, Bc1, Bc3);
|
||||
ENUM(FramePacingMode, Target_Auto, Target_30, Target_60, Target_90, Target_120);
|
||||
ENUM(VSyncMode, Immediate, Mailbox, Fifo, FifoRelaxed);
|
||||
ENUM(VramUsageMode, Conservative, Aggressive);
|
||||
ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV);
|
||||
@@ -156,6 +157,7 @@ ENUM(TemperatureUnits, Celsius, Fahrenheit)
|
||||
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
|
||||
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
|
||||
ENUM(GameListMode, TreeView, GridView);
|
||||
ENUM(SpeedMode, Standard, Turbo, Slow);
|
||||
|
||||
template <typename Type>
|
||||
inline std::string_view CanonicalizeEnum(Type id) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <thread>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <intrin.h>
|
||||
#endif
|
||||
|
||||
#include "common/x64/cpu_detect.h"
|
||||
#include "common/x64/cpu_wait.h"
|
||||
#include "common/x64/rdtsc.h"
|
||||
|
||||
namespace Common::X64 {
|
||||
|
||||
namespace {
|
||||
|
||||
// 100,000 cycles is a reasonable amount of time to wait to save on CPU resources.
|
||||
// For reference:
|
||||
// At 1 GHz, 100K cycles is 100us
|
||||
// At 2 GHz, 100K cycles is 50us
|
||||
// At 4 GHz, 100K cycles is 25us
|
||||
constexpr auto PauseCycles = 100'000U;
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
#if defined(_MSC_VER) && !defined(__clang__)
|
||||
__forceinline static void TPAUSE() {
|
||||
static constexpr auto RequestC02State = 0U;
|
||||
_tpause(RequestC02State, FencedRDTSC() + PauseCycles);
|
||||
}
|
||||
|
||||
__forceinline static void MWAITX() {
|
||||
static constexpr auto EnableWaitTimeFlag = 1U << 1;
|
||||
static constexpr auto RequestC1State = 0U;
|
||||
|
||||
// monitor_var should be aligned to a cache line.
|
||||
alignas(64) u64 monitor_var{};
|
||||
_mm_monitorx(&monitor_var, 0, 0);
|
||||
_mm_mwaitx(EnableWaitTimeFlag, RequestC1State, PauseCycles);
|
||||
}
|
||||
#else
|
||||
static void TPAUSE() {
|
||||
static constexpr auto RequestC02State = 0U;
|
||||
const auto tsc = FencedRDTSC() + PauseCycles;
|
||||
const auto eax = static_cast<u32>(tsc & 0xFFFFFFFF);
|
||||
const auto edx = static_cast<u32>(tsc >> 32);
|
||||
asm volatile("tpause %0" : : "r"(RequestC02State), "d"(edx), "a"(eax));
|
||||
}
|
||||
|
||||
static void MWAITX() {
|
||||
static constexpr auto EnableWaitTimeFlag = 1U << 1;
|
||||
static constexpr auto RequestC1State = 0U;
|
||||
|
||||
// monitor_var should be aligned to a cache line.
|
||||
alignas(64) u64 monitor_var{};
|
||||
asm volatile("monitorx" : : "a"(&monitor_var), "c"(0), "d"(0));
|
||||
asm volatile("mwaitx" : : "a"(RequestC1State), "b"(PauseCycles), "c"(EnableWaitTimeFlag));
|
||||
}
|
||||
#endif
|
||||
|
||||
void MicroSleep() {
|
||||
static const bool has_waitpkg = GetCPUCaps().waitpkg;
|
||||
static const bool has_monitorx = GetCPUCaps().monitorx;
|
||||
|
||||
if (has_waitpkg) {
|
||||
TPAUSE();
|
||||
} else if (has_monitorx) {
|
||||
MWAITX();
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Common::X64
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Common::X64 {
|
||||
|
||||
void MicroSleep();
|
||||
|
||||
} // namespace Common::X64
|
||||
@@ -1219,6 +1219,7 @@ target_link_libraries(core PRIVATE
|
||||
fmt::fmt
|
||||
nlohmann_json::nlohmann_json
|
||||
RenderDoc::API
|
||||
ZLIB::ZLIB
|
||||
MbedTLS::mbedcrypto${MBEDTLS_LIB_SUFFIX}
|
||||
MbedTLS::mbedtls${MBEDTLS_LIB_SUFFIX})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
@@ -38,10 +38,16 @@ u64 DynarmicCallbacks32::MemoryRead64(u32 vaddr) {
|
||||
CheckMemoryAccess(vaddr, 8, Kernel::DebugWatchpointType::Read);
|
||||
return m_memory.Read64(vaddr);
|
||||
}
|
||||
|
||||
std::optional<u32> DynarmicCallbacks32::MemoryReadCode(u32 vaddr) {
|
||||
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
|
||||
return std::nullopt;
|
||||
return m_memory.Read32(vaddr);
|
||||
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
|
||||
if (last_code_addr != aligned_vaddr) {
|
||||
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
|
||||
last_code_addr = aligned_vaddr;
|
||||
}
|
||||
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
|
||||
}
|
||||
|
||||
void DynarmicCallbacks32::MemoryWrite8(u32 vaddr, u8 value) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <dynarmic/interface/A32/a32.h>
|
||||
#include <dynarmic/interface/code_page.h>
|
||||
|
||||
#include "core/arm/arm_interface.h"
|
||||
#include "core/arm/dynarmic/dynarmic_exclusive_monitor.h"
|
||||
@@ -49,6 +50,9 @@ public:
|
||||
u64 GetTicksRemaining() override;
|
||||
bool CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type);
|
||||
void ReturnException(u32 pc, Dynarmic::HaltReason hr);
|
||||
//
|
||||
Dynarmic::CodePage cached_code_page;
|
||||
u64 last_code_addr = 0;
|
||||
ArmDynarmic32& m_parent;
|
||||
Core::Memory::Memory& m_memory;
|
||||
Kernel::KProcess* m_process{};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
@@ -41,10 +41,17 @@ Dynarmic::A64::Vector DynarmicCallbacks64::MemoryRead128(u64 vaddr) {
|
||||
CheckMemoryAccess(vaddr, 16, Kernel::DebugWatchpointType::Read);
|
||||
return {m_memory.Read64(vaddr), m_memory.Read64(vaddr + 8)};
|
||||
}
|
||||
|
||||
std::optional<u32> DynarmicCallbacks64::MemoryReadCode(u64 vaddr) {
|
||||
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
|
||||
return std::nullopt;
|
||||
return m_memory.Read32(vaddr);
|
||||
// return m_memory.Read32(vaddr);
|
||||
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
|
||||
if (last_code_addr != aligned_vaddr) {
|
||||
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
|
||||
last_code_addr = aligned_vaddr;
|
||||
}
|
||||
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
|
||||
}
|
||||
|
||||
void DynarmicCallbacks64::MemoryWrite8(u64 vaddr, u8 value) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <ankerl/unordered_dense.h>
|
||||
|
||||
#include <dynarmic/interface/A64/a64.h>
|
||||
#include <dynarmic/interface/code_page.h>
|
||||
#include "common/common_types.h"
|
||||
#include "common/hash.h"
|
||||
#include "core/arm/arm_interface.h"
|
||||
@@ -61,6 +62,8 @@ public:
|
||||
bool CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type);
|
||||
void ReturnException(u64 pc, Dynarmic::HaltReason hr);
|
||||
|
||||
Dynarmic::CodePage cached_code_page;
|
||||
u64 last_code_addr = 0;
|
||||
ArmDynarmic64& m_parent;
|
||||
Core::Memory::Memory& m_memory;
|
||||
u64 m_tpidrro_el0{};
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#include "common/windows/timer_resolution.h"
|
||||
#endif
|
||||
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
#include "common/x64/cpu_wait.h"
|
||||
#endif
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/hardware_properties.h"
|
||||
@@ -201,7 +205,7 @@ u64 CoreTiming::GetClockTicks() const {
|
||||
|
||||
if (Settings::values.sync_core_speed.GetValue()) {
|
||||
const auto ticks = double(fres);
|
||||
const auto speed_limit = double(Settings::values.speed_limit.GetValue())*0.01;
|
||||
const auto speed_limit = double(Settings::SpeedLimit())*0.01;
|
||||
return u64(ticks/speed_limit);
|
||||
} else {
|
||||
return fres;
|
||||
@@ -283,7 +287,28 @@ void CoreTiming::ThreadLoop() {
|
||||
if (next_time) {
|
||||
// There are more events left in the queue, wait until the next event.
|
||||
auto wait_time = *next_time - GetGlobalTimeNs().count();
|
||||
event.WaitFor(std::chrono::nanoseconds(wait_time));
|
||||
if (wait_time > 0) {
|
||||
#ifdef _WIN32
|
||||
while (!paused && !event.IsSet() && wait_time > 0) {
|
||||
wait_time = *next_time - GetGlobalTimeNs().count();
|
||||
if (wait_time >= timer_resolution_ns) {
|
||||
Common::Windows::SleepForOneTick();
|
||||
} else {
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
Common::X64::MicroSleep();
|
||||
#else
|
||||
std::this_thread::yield();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (event.IsSet()) {
|
||||
event.Reset();
|
||||
}
|
||||
#else
|
||||
event.WaitFor(std::chrono::nanoseconds(wait_time));
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
// Queue is empty, wait until another event is scheduled and signals us to
|
||||
// continue.
|
||||
|
||||
@@ -127,6 +127,11 @@ public:
|
||||
void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta);
|
||||
|
||||
private:
|
||||
struct TranslationEntry {
|
||||
DAddr guest_page{};
|
||||
u8* host_ptr{};
|
||||
};
|
||||
|
||||
// Internal helper that performs the update assuming the caller already holds the necessary lock.
|
||||
void UpdatePagesCachedCountNoLock(DAddr addr, size_t size, s32 delta);
|
||||
|
||||
@@ -195,6 +200,8 @@ private:
|
||||
}
|
||||
|
||||
Common::VirtualBuffer<VAddr> cpu_backing_address;
|
||||
std::array<TranslationEntry, 4> t_slot{};
|
||||
u32 cache_cursor = 0;
|
||||
using CounterType = u8;
|
||||
using CounterAtomicType = std::atomic_uint8_t;
|
||||
static constexpr size_t subentries = 8 / sizeof(CounterType);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
@@ -247,6 +247,7 @@ void DeviceMemoryManager<Traits>::Map(DAddr address, VAddr virtual_address, size
|
||||
}
|
||||
impl->multi_dev_address.Register(new_dev, start_id);
|
||||
}
|
||||
t_slot = {};
|
||||
if (track) {
|
||||
TrackContinuityImpl(address, virtual_address, size, asid);
|
||||
}
|
||||
@@ -278,6 +279,7 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) {
|
||||
compressed_device_addr[phys_addr - 1] = new_start | MULTI_FLAG;
|
||||
}
|
||||
}
|
||||
t_slot = {};
|
||||
}
|
||||
template <typename Traits>
|
||||
void DeviceMemoryManager<Traits>::TrackContinuityImpl(DAddr address, VAddr virtual_address,
|
||||
@@ -417,6 +419,26 @@ void DeviceMemoryManager<Traits>::WalkBlock(DAddr addr, std::size_t size, auto o
|
||||
template <typename Traits>
|
||||
void DeviceMemoryManager<Traits>::ReadBlock(DAddr address, void* dest_pointer, size_t size) {
|
||||
device_inter->FlushRegion(address, size);
|
||||
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
|
||||
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
|
||||
const DAddr guest_page = address & ~static_cast<DAddr>(Memory::YUZU_PAGEMASK);
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
|
||||
std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t page_index = address >> Memory::YUZU_PAGEBITS;
|
||||
const auto phys_addr = compressed_physical_ptr[page_index];
|
||||
if (phys_addr != 0) {
|
||||
auto* const mem_ptr = GetPointerFromRaw<u8>((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
|
||||
t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
|
||||
cache_cursor = (cache_cursor + 1) & 3U;
|
||||
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
|
||||
return;
|
||||
}
|
||||
}
|
||||
WalkBlock(
|
||||
address, size,
|
||||
[&](size_t copy_amount, DAddr current_vaddr) {
|
||||
@@ -455,6 +477,26 @@ void DeviceMemoryManager<Traits>::WriteBlock(DAddr address, const void* src_poin
|
||||
|
||||
template <typename Traits>
|
||||
void DeviceMemoryManager<Traits>::ReadBlockUnsafe(DAddr address, void* dest_pointer, size_t size) {
|
||||
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
|
||||
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
|
||||
const DAddr guest_page = address & ~static_cast<DAddr>(Memory::YUZU_PAGEMASK);
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
|
||||
std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t page_index = address >> Memory::YUZU_PAGEBITS;
|
||||
const auto phys_addr = compressed_physical_ptr[page_index];
|
||||
if (phys_addr != 0) {
|
||||
auto* const mem_ptr = GetPointerFromRaw<u8>((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
|
||||
t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
|
||||
cache_cursor = (cache_cursor + 1) & 3U;
|
||||
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
|
||||
return;
|
||||
}
|
||||
}
|
||||
WalkBlock(
|
||||
address, size,
|
||||
[&](size_t copy_amount, DAddr current_vaddr) {
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <zlib.h>
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/swap.h"
|
||||
@@ -31,6 +38,73 @@ const std::array<const char*, 16> LANGUAGE_NAMES{{
|
||||
"BrazilianPortuguese",
|
||||
}};
|
||||
|
||||
namespace {
|
||||
constexpr std::size_t LEGACY_LANGUAGE_REGION_SIZE = sizeof(std::array<LanguageEntry, 16>);
|
||||
constexpr std::size_t PACKED_LANGUAGE_REGION_MAX_SIZE = sizeof(LanguageEntry) * 32;
|
||||
|
||||
bool InflateRawDeflate(std::span<const u8> compressed, std::vector<u8>& out) {
|
||||
if (compressed.empty() || compressed.size() > std::numeric_limits<uInt>::max()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
z_stream stream{};
|
||||
stream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(compressed.data()));
|
||||
stream.avail_in = static_cast<uInt>(compressed.size());
|
||||
if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<u8, 0x1000> chunk{};
|
||||
int ret = Z_OK;
|
||||
while (ret == Z_OK) {
|
||||
stream.next_out = reinterpret_cast<Bytef*>(chunk.data());
|
||||
stream.avail_out = static_cast<uInt>(chunk.size());
|
||||
ret = inflate(&stream, Z_NO_FLUSH);
|
||||
if (ret != Z_OK && ret != Z_STREAM_END) {
|
||||
inflateEnd(&stream);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto produced = chunk.size() - static_cast<std::size_t>(stream.avail_out);
|
||||
if (produced != 0) {
|
||||
if (out.size() + produced > PACKED_LANGUAGE_REGION_MAX_SIZE) {
|
||||
inflateEnd(&stream);
|
||||
return false;
|
||||
}
|
||||
out.insert(out.end(), chunk.begin(),
|
||||
chunk.begin() + static_cast<std::ptrdiff_t>(produced));
|
||||
}
|
||||
}
|
||||
|
||||
inflateEnd(&stream);
|
||||
return ret == Z_STREAM_END;
|
||||
}
|
||||
|
||||
void DecodePackedLanguageEntries(RawNACP& raw) {
|
||||
auto* packed_region = reinterpret_cast<u8*>(raw.language_entries.data());
|
||||
u16_le compressed_size_le{};
|
||||
std::memcpy(&compressed_size_le, packed_region, sizeof(compressed_size_le));
|
||||
const auto compressed_size = static_cast<std::size_t>(compressed_size_le);
|
||||
|
||||
if (compressed_size == 0 || compressed_size > LEGACY_LANGUAGE_REGION_SIZE - sizeof(u16_le)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<u8> decompressed;
|
||||
if (!InflateRawDeflate(
|
||||
std::span<const u8>(packed_region + sizeof(u16_le), compressed_size), decompressed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (decompressed.size() < LEGACY_LANGUAGE_REGION_SIZE ||
|
||||
decompressed.size() % sizeof(LanguageEntry) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::memcpy(raw.language_entries.data(), decompressed.data(), LEGACY_LANGUAGE_REGION_SIZE);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string LanguageEntry::GetApplicationName() const {
|
||||
return Common::StringFromFixedZeroTerminatedBuffer(application_name.data(),
|
||||
application_name.size());
|
||||
@@ -66,6 +140,7 @@ NACP::NACP() = default;
|
||||
|
||||
NACP::NACP(VirtualFile file) {
|
||||
file->ReadObject(&raw);
|
||||
DecodePackedLanguageEntries(raw);
|
||||
}
|
||||
|
||||
NACP::~NACP() = default;
|
||||
|
||||
@@ -345,11 +345,7 @@ void FileSystemController::SetPackedUpdate(ProcessId process_id, FileSys::Virtua
|
||||
}
|
||||
|
||||
std::shared_ptr<SaveDataController> FileSystemController::OpenSaveDataController() {
|
||||
if (!system_save_data_controller) {
|
||||
system_save_data_controller =
|
||||
std::make_shared<SaveDataController>(system, CreateSaveDataFactory(ProgramId{}));
|
||||
}
|
||||
return system_save_data_controller;
|
||||
return std::make_shared<SaveDataController>(system, CreateSaveDataFactory(ProgramId{}));
|
||||
}
|
||||
|
||||
std::shared_ptr<FileSys::SaveDataFactory> FileSystemController::CreateSaveDataFactory(
|
||||
|
||||
@@ -151,8 +151,6 @@ private:
|
||||
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
|
||||
|
||||
Core::System& system;
|
||||
|
||||
std::shared_ptr<SaveDataController> system_save_data_controller;
|
||||
};
|
||||
|
||||
void LoopProcess(Core::System& system);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <boost/icl/interval_set.hpp>
|
||||
#include <dynarmic/interface/A64/a64.h>
|
||||
#include <dynarmic/interface/A64/config.h>
|
||||
#include <dynarmic/interface/code_page.h>
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/common_funcs.h"
|
||||
@@ -46,6 +47,15 @@ public:
|
||||
: memory{memory_}, local_memory{local_memory_},
|
||||
mapped_ranges{mapped_ranges_}, parent{parent_} {}
|
||||
|
||||
std::optional<std::uint32_t> MemoryReadCode(VAddr vaddr) override {
|
||||
static_assert(Core::Memory::YUZU_PAGESIZE == Dynarmic::CODE_PAGE_SIZE);
|
||||
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
|
||||
if (last_code_addr != aligned_vaddr) {
|
||||
cached_code_page = ReadMemory<Dynarmic::CodePage>(aligned_vaddr);
|
||||
last_code_addr = aligned_vaddr;
|
||||
}
|
||||
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
|
||||
}
|
||||
u8 MemoryRead8(u64 vaddr) override {
|
||||
return ReadMemory<u8>(vaddr);
|
||||
}
|
||||
@@ -116,8 +126,7 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T ReadMemory(u64 vaddr) {
|
||||
template<typename T> T ReadMemory(u64 vaddr) {
|
||||
T ret{};
|
||||
if (boost::icl::contains(mapped_ranges, vaddr)) {
|
||||
memory.ReadBlock(vaddr, &ret, sizeof(T));
|
||||
@@ -146,6 +155,9 @@ private:
|
||||
std::vector<u8>& local_memory;
|
||||
IntervalSet& mapped_ranges;
|
||||
JITContextImpl& parent;
|
||||
|
||||
Dynarmic::CodePage cached_code_page;
|
||||
u64 last_code_addr = 0;
|
||||
};
|
||||
|
||||
class JITContextImpl {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
@@ -980,25 +980,82 @@ Result ISystemSettingsServer::SetPrimaryAlbumStorage(PrimaryAlbumStorage primary
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
static void Fill3DS_CRC(u32 d, char* data) {
|
||||
std::array<u8, 10> digits = {
|
||||
u8((d / 1000000000) % 100),
|
||||
u8((d / 100000000) % 10),
|
||||
u8((d / 10000000) % 10),
|
||||
u8((d / 1000000) % 10),
|
||||
u8((d / 100000) % 10),
|
||||
u8((d / 10000) % 10),
|
||||
u8((d / 1000) % 10),
|
||||
u8((d / 100) % 10),
|
||||
u8((d / 10) % 10),
|
||||
u8(d % 10),
|
||||
};
|
||||
// Normalize to retail values
|
||||
std::array<u8, 4> retail_digits = { 1, 4, 5, 7 };
|
||||
digits[0] = retail_digits[(d % 10) % 4];
|
||||
digits[1] = 0;
|
||||
//
|
||||
for (size_t i = 0; i < sizeof(digits); ++i)
|
||||
data[i] = char(digits[i] + '0');
|
||||
u8 sum_odd = 0, sum_even = 0;
|
||||
for (size_t i = 0; i < sizeof(digits); i += 2) {
|
||||
sum_odd += digits[i + 0];
|
||||
sum_even += digits[i + 1];
|
||||
}
|
||||
u8 sum_digit = u8(((sum_even * 3) + sum_odd) % 10);
|
||||
if (sum_digit != 0)
|
||||
sum_digit = 10 - sum_digit;
|
||||
data[sizeof(digits)] = char(sum_digit + '0');
|
||||
}
|
||||
|
||||
Result ISystemSettingsServer::GetBatteryLot(Out<BatteryLot> out_battery_lot) {
|
||||
LOG_INFO(Service_SET, "called");
|
||||
*out_battery_lot = {"YUZU0EMULATOR14022024"};
|
||||
if (auto const s = ::Settings::values.serial_battery.GetValue(); !s.empty()) {
|
||||
auto const max_size = out_battery_lot->lot_number.size();
|
||||
auto const end = s.size() > max_size ? s.begin() + max_size : s.end();
|
||||
std::copy(s.begin(), end, out_battery_lot->lot_number.begin());
|
||||
}
|
||||
*out_battery_lot = []{
|
||||
u32 d = ::Settings::values.serial_battery.GetValue();
|
||||
BatteryLot c{};
|
||||
c.lot_number[0] = 'B';
|
||||
c.lot_number[1] = 'H';
|
||||
c.lot_number[2] = 'A';
|
||||
c.lot_number[3] = 'C';
|
||||
// TODO: I have no fucking idea what the letters mean
|
||||
c.lot_number[4] = 'H';
|
||||
c.lot_number[5] = 'Z';
|
||||
c.lot_number[6] = 'Z';
|
||||
c.lot_number[7] = 'A';
|
||||
c.lot_number[8] = 'D';
|
||||
c.lot_number[9] = char(((d / 100000) % 26) + 'A');
|
||||
Fill3DS_CRC(d, c.lot_number.data() + 10);
|
||||
return c;
|
||||
}();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ISystemSettingsServer::GetSerialNumber(Out<SerialNumber> out_console_serial) {
|
||||
LOG_INFO(Service_SET, "called");
|
||||
*out_console_serial = {"YUZ10000000001"};
|
||||
if (auto const s = ::Settings::values.serial_unit.GetValue(); !s.empty()) {
|
||||
auto const max_size = out_console_serial->serial_number.size();
|
||||
auto const end = s.size() > max_size ? s.begin() + max_size : s.end();
|
||||
std::copy(s.begin(), end, out_console_serial->serial_number.begin());
|
||||
}
|
||||
*out_console_serial = []{
|
||||
u32 d = ::Settings::values.serial_unit.GetValue();
|
||||
SerialNumber c{};
|
||||
c.serial_number[0] = 'X';
|
||||
c.serial_number[1] = 'A';
|
||||
c.serial_number[2] = [] {
|
||||
// Adding another setting would be tedious so... let's just reuse region_index :)
|
||||
switch (::Settings::values.region_index.GetValue()) {
|
||||
case ::Settings::Region::Japan: return 'J';
|
||||
case ::Settings::Region::Usa: return 'W';
|
||||
case ::Settings::Region::Europe: return 'E';
|
||||
case ::Settings::Region::Australia: return 'M'; //pretend its Malaysia
|
||||
case ::Settings::Region::China:
|
||||
case ::Settings::Region::Taiwan: return 'C';
|
||||
case ::Settings::Region::Korea: return 'K';
|
||||
default: return 'W';
|
||||
}
|
||||
}();
|
||||
Fill3DS_CRC(d, c.serial_number.data() + 3);
|
||||
return c;
|
||||
}();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
@@ -178,7 +178,7 @@ void LoopProcess(Core::System& system) {
|
||||
auto module = std::make_shared<Module>();
|
||||
|
||||
server_manager->RegisterNamedService("csrng", std::make_shared<CSRNG>(system, module));
|
||||
server_manager->RegisterNamedService("spl", std::make_shared<SPL>(system, module));
|
||||
server_manager->RegisterNamedService("spl:", std::make_shared<SPL>(system, module));
|
||||
server_manager->RegisterNamedService("spl:mig", std::make_shared<SPL_MIG>(system, module));
|
||||
server_manager->RegisterNamedService("spl:fs", std::make_shared<SPL_FS>(system, module));
|
||||
server_manager->RegisterNamedService("spl:ssl", std::make_shared<SPL_SSL>(system, module));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
@@ -95,7 +95,7 @@ s64 Conductor::GetNextTicks() const {
|
||||
if (settings.use_speed_limit.GetValue()) {
|
||||
// Scales the speed based on speed_limit setting on MC. SC is handled by
|
||||
// SpeedLimiter::DoSpeedLimiting.
|
||||
speed_scale = 100.f / settings.speed_limit.GetValue();
|
||||
speed_scale = 100.f / Settings::SpeedLimit();
|
||||
} else {
|
||||
// Run at unlocked framerate.
|
||||
speed_scale = 0.01f;
|
||||
|
||||
+2
-3
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2015 Citra Emulator Project
|
||||
@@ -36,8 +36,7 @@
|
||||
|
||||
namespace Core::Memory {
|
||||
|
||||
static inline bool AddressSpaceContains(const Common::PageTable& table, const Common::ProcessAddress addr,
|
||||
const std::size_t size) {
|
||||
static inline bool AddressSpaceContains(const Common::PageTable& table, const Common::ProcessAddress addr, const std::size_t size) {
|
||||
const Common::ProcessAddress max_addr = 1ULL << table.GetAddressSpaceBits();
|
||||
return addr + size >= addr && addr + size <= max_addr;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
@@ -143,7 +143,7 @@ void SpeedLimiter::DoSpeedLimiting(microseconds current_system_time_us) {
|
||||
|
||||
auto now = Clock::now();
|
||||
|
||||
const double sleep_scale = Settings::values.speed_limit.GetValue() / 100.0;
|
||||
const double sleep_scale = Settings::SpeedLimit() / 100.0;
|
||||
|
||||
// Max lag caused by slow frames. Shouldn't be more than the length of a frame at the current
|
||||
// speed percent or it will clamp too much and prevent this from properly limiting to that
|
||||
|
||||
@@ -15,8 +15,6 @@ add_library(dynarmic STATIC
|
||||
common/crypto/aes.h
|
||||
common/crypto/crc32.cpp
|
||||
common/crypto/crc32.h
|
||||
common/crypto/sm4.cpp
|
||||
common/crypto/sm4.h
|
||||
common/fp/fpcr.h
|
||||
common/fp/fpsr.h
|
||||
common/fp/fused.cpp
|
||||
@@ -107,9 +105,6 @@ add_library(dynarmic STATIC
|
||||
frontend/A32/decoder/thumb32.inc
|
||||
frontend/A32/decoder/vfp.h
|
||||
frontend/A32/decoder/vfp.inc
|
||||
frontend/A32/disassembler/disassembler.h
|
||||
frontend/A32/disassembler/disassembler_arm.cpp
|
||||
frontend/A32/disassembler/disassembler_thumb.cpp
|
||||
frontend/A32/FPSCR.h
|
||||
frontend/A32/ITState.h
|
||||
frontend/A32/PSR.h
|
||||
@@ -124,7 +119,6 @@ add_library(dynarmic STATIC
|
||||
interface/A32/config.h
|
||||
interface/A32/coprocessor.h
|
||||
interface/A32/coprocessor_util.h
|
||||
interface/A32/disassembler.h
|
||||
# A64
|
||||
frontend/A64/a64_ir_emitter.cpp
|
||||
frontend/A64/a64_ir_emitter.h
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -164,10 +164,10 @@ A32AddressSpace::A32AddressSpace(const A32::UserConfig& conf)
|
||||
EmitPrelude();
|
||||
}
|
||||
|
||||
IR::Block A32AddressSpace::GenerateIR(IR::LocationDescriptor descriptor) const {
|
||||
IR::Block ir_block = A32::Translate(A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
void A32AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
ir_block.Reset(descriptor);
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
Optimization::Optimize(ir_block, conf, {});
|
||||
return ir_block;
|
||||
}
|
||||
|
||||
void A32AddressSpace::InvalidateCacheRanges(const boost::icl::interval_set<u32>& ranges) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2022 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
@@ -17,7 +20,7 @@ class A32AddressSpace final : public AddressSpace {
|
||||
public:
|
||||
explicit A32AddressSpace(const A32::UserConfig& conf);
|
||||
|
||||
IR::Block GenerateIR(IR::LocationDescriptor) const override;
|
||||
void GenerateIR(IR::Block& ir_block, IR::LocationDescriptor) const override;
|
||||
|
||||
void InvalidateCacheRanges(const boost::icl::interval_set<u32>& ranges);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -330,12 +330,11 @@ A64AddressSpace::A64AddressSpace(const A64::UserConfig& conf)
|
||||
EmitPrelude();
|
||||
}
|
||||
|
||||
IR::Block A64AddressSpace::GenerateIR(IR::LocationDescriptor descriptor) const {
|
||||
void A64AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
ir_block.Reset(descriptor);
|
||||
const auto get_code = [this](u64 vaddr) { return conf.callbacks->MemoryReadCode(vaddr); };
|
||||
IR::Block ir_block = A64::Translate(A64::LocationDescriptor{descriptor}, get_code,
|
||||
{conf.define_unpredictable_behaviour, conf.wall_clock_cntpct});
|
||||
A64::Translate(ir_block, A64::LocationDescriptor{descriptor}, get_code, {conf.define_unpredictable_behaviour, conf.wall_clock_cntpct});
|
||||
Optimization::Optimize(ir_block, conf, {});
|
||||
return ir_block;
|
||||
}
|
||||
|
||||
void A64AddressSpace::InvalidateCacheRanges(const boost::icl::interval_set<u64>& ranges) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2022 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
@@ -17,7 +20,7 @@ class A64AddressSpace final : public AddressSpace {
|
||||
public:
|
||||
explicit A64AddressSpace(const A64::UserConfig& conf);
|
||||
|
||||
IR::Block GenerateIR(IR::LocationDescriptor) const override;
|
||||
void GenerateIR(IR::Block& ir_block, IR::LocationDescriptor) const override;
|
||||
|
||||
void InvalidateCacheRanges(const boost::icl::interval_set<u64>& ranges);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -20,14 +20,17 @@
|
||||
#include "dynarmic/common/fp/fpcr.h"
|
||||
#include "dynarmic/common/llvm_disassemble.h"
|
||||
#include "dynarmic/interface/exclusive_monitor.h"
|
||||
#include "dynarmic/ir/location_descriptor.h"
|
||||
|
||||
namespace Dynarmic::Backend::Arm64 {
|
||||
|
||||
AddressSpace::AddressSpace(size_t code_cache_size)
|
||||
: code_cache_size(code_cache_size)
|
||||
, mem(code_cache_size)
|
||||
, code(mem.ptr(), mem.ptr())
|
||||
, fastmem_manager(exception_handler) {
|
||||
: ir_block{IR::LocationDescriptor{0}}
|
||||
, code_cache_size(code_cache_size)
|
||||
, mem(code_cache_size)
|
||||
, code(mem.ptr(), mem.ptr())
|
||||
, fastmem_manager(exception_handler)
|
||||
{
|
||||
ASSERT(code_cache_size <= 128 * 1024 * 1024 && "code_cache_size > 128 MiB not currently supported");
|
||||
|
||||
exception_handler.Register(mem, code_cache_size);
|
||||
@@ -67,8 +70,7 @@ CodePtr AddressSpace::GetOrEmit(IR::LocationDescriptor descriptor) {
|
||||
if (CodePtr block_entry = Get(descriptor)) {
|
||||
return block_entry;
|
||||
}
|
||||
|
||||
IR::Block ir_block = GenerateIR(descriptor);
|
||||
GenerateIR(ir_block, descriptor);
|
||||
const EmittedBlockInfo block_info = Emit(std::move(ir_block));
|
||||
return block_info.entry_point;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -29,7 +29,7 @@ public:
|
||||
explicit AddressSpace(size_t code_cache_size);
|
||||
virtual ~AddressSpace();
|
||||
|
||||
virtual IR::Block GenerateIR(IR::LocationDescriptor) const = 0;
|
||||
virtual void GenerateIR(IR::Block& ir_block, IR::LocationDescriptor) const = 0;
|
||||
|
||||
CodePtr Get(IR::LocationDescriptor descriptor);
|
||||
|
||||
@@ -68,6 +68,7 @@ protected:
|
||||
|
||||
FakeCall FastmemCallback(u64 host_pc);
|
||||
|
||||
IR::Block ir_block;
|
||||
const size_t code_cache_size;
|
||||
oaknut::CodeBlock mem;
|
||||
oaknut::CodeGenerator code;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -26,10 +26,9 @@ A32AddressSpace::A32AddressSpace(const A32::UserConfig& conf)
|
||||
EmitPrelude();
|
||||
}
|
||||
|
||||
IR::Block A32AddressSpace::GenerateIR(IR::LocationDescriptor descriptor) const {
|
||||
IR::Block ir_block = A32::Translate(A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
void A32AddressSpace::GenerateIR(IR::Block& ir_block, IR::LocationDescriptor descriptor) const {
|
||||
A32::Translate(ir_block, A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
Optimization::Optimize(ir_block, conf, {});
|
||||
return ir_block;
|
||||
}
|
||||
|
||||
CodePtr A32AddressSpace::Get(IR::LocationDescriptor descriptor) {
|
||||
@@ -44,7 +43,8 @@ CodePtr A32AddressSpace::GetOrEmit(IR::LocationDescriptor descriptor) {
|
||||
return block_entry;
|
||||
}
|
||||
|
||||
IR::Block ir_block = GenerateIR(descriptor);
|
||||
IR::Block ir_block{descriptor};
|
||||
GenerateIR(ir_block, descriptor);
|
||||
const EmittedBlockInfo block_info = Emit(std::move(ir_block));
|
||||
|
||||
block_infos.insert_or_assign(descriptor.Value(), block_info);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
* Copyright (c) 2024 MerryMage
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
@@ -23,7 +26,7 @@ class A32AddressSpace final {
|
||||
public:
|
||||
explicit A32AddressSpace(const A32::UserConfig& conf);
|
||||
|
||||
IR::Block GenerateIR(IR::LocationDescriptor) const;
|
||||
void GenerateIR(IR::Block& ir_block, IR::LocationDescriptor) const;
|
||||
|
||||
CodePtr Get(IR::LocationDescriptor descriptor);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -14,7 +14,7 @@
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ostream.h>
|
||||
#include <mcl/type_traits/integer_of_size.hpp>
|
||||
#include <xbyak/xbyak.h>
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
|
||||
#include "dynarmic/backend/x64/a32_emit_x64.h"
|
||||
#include "dynarmic/backend/x64/abi.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "dynarmic/backend/x64/devirtualize.h"
|
||||
#include "dynarmic/backend/x64/jitstate_info.h"
|
||||
#include "dynarmic/common/atomic.h"
|
||||
#include "dynarmic/frontend/A32/a32_location_descriptor.h"
|
||||
#include "dynarmic/frontend/A32/translate/a32_translate.h"
|
||||
#include "dynarmic/interface/A32/a32.h"
|
||||
#include "dynarmic/ir/basic_block.h"
|
||||
@@ -63,12 +64,13 @@ static Optimization::PolyfillOptions GenPolyfillOptions(const BlockOfCode& code)
|
||||
}
|
||||
|
||||
struct Jit::Impl {
|
||||
Impl(Jit* jit, A32::UserConfig conf)
|
||||
: block_of_code(GenRunCodeCallbacks(conf.callbacks, &GetCurrentBlockThunk, this, conf), JitStateInfo{jit_state}, conf.code_cache_size, GenRCP(conf))
|
||||
, emitter(block_of_code, conf, jit)
|
||||
, polyfill_options(GenPolyfillOptions(block_of_code))
|
||||
, conf(std::move(conf))
|
||||
, jit_interface(jit) {}
|
||||
Impl(Jit* jit, A32::UserConfig conf) noexcept
|
||||
: block_of_code(GenRunCodeCallbacks(conf.callbacks, &GetCurrentBlockThunk, this, conf), JitStateInfo{jit_state}, conf.code_cache_size, GenRCP(conf))
|
||||
, emitter(block_of_code, conf, jit)
|
||||
, polyfill_options(GenPolyfillOptions(block_of_code))
|
||||
, conf(std::move(conf))
|
||||
, jit_interface(jit)
|
||||
{}
|
||||
|
||||
~Impl() = default;
|
||||
|
||||
@@ -201,8 +203,7 @@ private:
|
||||
}
|
||||
|
||||
A32EmitX64::BlockDescriptor GetBasicBlock(IR::LocationDescriptor descriptor) {
|
||||
auto block = emitter.GetBasicBlock(descriptor);
|
||||
if (block)
|
||||
if (auto block = emitter.GetBasicBlock(descriptor))
|
||||
return *block;
|
||||
|
||||
constexpr size_t MINIMUM_REMAINING_CODESIZE = 1 * 1024 * 1024;
|
||||
@@ -212,7 +213,10 @@ private:
|
||||
}
|
||||
block_of_code.EnsureMemoryCommitted(MINIMUM_REMAINING_CODESIZE);
|
||||
|
||||
IR::Block ir_block = A32::Translate(A32::LocationDescriptor{descriptor}, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
// LocationDescriptor ctor() does important ops (like tflags) do not skip
|
||||
auto const arch_descriptor = A32::LocationDescriptor{descriptor};
|
||||
ir_block.Reset(arch_descriptor);
|
||||
A32::Translate(ir_block, arch_descriptor, conf.callbacks, {conf.arch_version, conf.define_unpredictable_behaviour, conf.hook_hint_instructions});
|
||||
Optimization::Optimize(ir_block, conf, polyfill_options);
|
||||
return emitter.Emit(ir_block);
|
||||
}
|
||||
@@ -239,13 +243,13 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
IR::Block ir_block = {LocationDescriptor(0, PSR(0), FPSCR(0), false)};
|
||||
A32JitState jit_state;
|
||||
BlockOfCode block_of_code;
|
||||
A32EmitX64 emitter;
|
||||
Optimization::PolyfillOptions polyfill_options;
|
||||
|
||||
// Keep it here, you don't wanna mess with the fuckery that's initializer lists
|
||||
const A32::UserConfig conf;
|
||||
|
||||
Jit* jit_interface;
|
||||
|
||||
// Requests made during execution to invalidate the cache are queued up here.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -14,7 +14,7 @@
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ostream.h>
|
||||
#include <mcl/type_traits/integer_of_size.hpp>
|
||||
#include <xbyak/xbyak.h>
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
|
||||
#include "dynarmic/backend/x64/a64_emit_x64.h"
|
||||
#include "dynarmic/backend/x64/abi.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <boost/icl/interval_set.hpp>
|
||||
#include "dynarmic/common/assert.h"
|
||||
#include "dynarmic/common/fp/fpcr.h"
|
||||
#include "dynarmic/common/llvm_disassemble.h"
|
||||
#include <bit>
|
||||
#include <mcl/scope_exit.hpp>
|
||||
@@ -61,10 +62,11 @@ static Optimization::PolyfillOptions GenPolyfillOptions(const BlockOfCode& code)
|
||||
struct Jit::Impl final {
|
||||
public:
|
||||
Impl(Jit* jit, UserConfig conf)
|
||||
: conf(conf)
|
||||
, block_of_code(GenRunCodeCallbacks(conf.callbacks, &GetCurrentBlockThunk, this, conf), JitStateInfo{jit_state}, conf.code_cache_size, GenRCP(conf))
|
||||
, emitter(block_of_code, conf, jit)
|
||||
, polyfill_options(GenPolyfillOptions(block_of_code)) {
|
||||
: conf(conf)
|
||||
, block_of_code(GenRunCodeCallbacks(conf.callbacks, &GetCurrentBlockThunk, this, conf), JitStateInfo{jit_state}, conf.code_cache_size, GenRCP(conf))
|
||||
, emitter(block_of_code, conf, jit)
|
||||
, polyfill_options(GenPolyfillOptions(block_of_code))
|
||||
{
|
||||
ASSERT(conf.page_table_address_space_bits >= 12 && conf.page_table_address_space_bits <= 64);
|
||||
}
|
||||
|
||||
@@ -254,8 +256,8 @@ private:
|
||||
return GetBlock(A64::LocationDescriptor{GetCurrentLocation()}.SetSingleStepping(true));
|
||||
}
|
||||
|
||||
CodePtr GetBlock(IR::LocationDescriptor current_location) {
|
||||
if (auto block = emitter.GetBasicBlock(current_location))
|
||||
CodePtr GetBlock(IR::LocationDescriptor descriptor) {
|
||||
if (auto block = emitter.GetBasicBlock(descriptor))
|
||||
return block->entrypoint;
|
||||
|
||||
constexpr size_t MINIMUM_REMAINING_CODESIZE = 1 * 1024 * 1024;
|
||||
@@ -268,8 +270,10 @@ private:
|
||||
|
||||
// JIT Compile
|
||||
const auto get_code = [this](u64 vaddr) { return conf.callbacks->MemoryReadCode(vaddr); };
|
||||
IR::Block ir_block = A64::Translate(A64::LocationDescriptor{current_location}, get_code,
|
||||
{conf.define_unpredictable_behaviour, conf.wall_clock_cntpct});
|
||||
// LocationDescriptor ctor() does important ops (like tflags) do not skip
|
||||
auto const arch_descriptor = A64::LocationDescriptor{descriptor};
|
||||
ir_block.Reset(arch_descriptor);
|
||||
A64::Translate(ir_block, arch_descriptor, get_code, {conf.define_unpredictable_behaviour, conf.wall_clock_cntpct});
|
||||
Optimization::Optimize(ir_block, conf, polyfill_options);
|
||||
return emitter.Emit(ir_block).entrypoint;
|
||||
}
|
||||
@@ -296,14 +300,13 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
bool is_executing = false;
|
||||
|
||||
IR::Block ir_block = {LocationDescriptor(0, FP::FPCR(0), false)};
|
||||
const UserConfig conf;
|
||||
A64JitState jit_state;
|
||||
BlockOfCode block_of_code;
|
||||
A64EmitX64 emitter;
|
||||
Optimization::PolyfillOptions polyfill_options;
|
||||
|
||||
bool is_executing = false;
|
||||
bool invalidate_entire_cache = false;
|
||||
boost::icl::interval_set<u64> invalid_cache_ranges;
|
||||
std::mutex invalidation_mutex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "dynarmic/common/common_types.h"
|
||||
#include <xbyak/xbyak.h>
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
|
||||
#include "dynarmic/backend/x64/block_of_code.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#include "dynarmic/common/assert.h"
|
||||
#include <mcl/bit/bit_field.hpp>
|
||||
#include <xbyak/xbyak.h>
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
|
||||
#include "dynarmic/backend/x64/a32_jitstate.h"
|
||||
#include "dynarmic/backend/x64/abi.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -15,9 +15,7 @@
|
||||
|
||||
#include <mcl/bit/bit_field.hpp>
|
||||
#include "dynarmic/common/common_types.h"
|
||||
#include <xbyak/xbyak.h>
|
||||
#include <xbyak/xbyak_util.h>
|
||||
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
#include "dynarmic/backend/x64/abi.h"
|
||||
#include "dynarmic/backend/x64/callback.h"
|
||||
#include "dynarmic/backend/x64/constant_pool.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "dynarmic/common/common_types.h"
|
||||
#include <xbyak/xbyak.h>
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
|
||||
namespace Dynarmic::Backend::X64 {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
#include "dynarmic/common/common_types.h"
|
||||
#include <ankerl/unordered_dense.h>
|
||||
#include <xbyak/xbyak.h>
|
||||
#include "dynarmic/backend/x64/xbyak.h"
|
||||
|
||||
namespace Dynarmic::Backend::X64 {
|
||||
|
||||
|
||||
@@ -71,11 +71,22 @@ void EmitX64::EmitBreakpoint(EmitContext&, IR::Inst*) {
|
||||
code.int3();
|
||||
}
|
||||
|
||||
constexpr bool IsWithin2G(uintptr_t ref, uintptr_t target) {
|
||||
const u64 distance = target - (ref + 5);
|
||||
return !(distance >= 0x8000'0000ULL && distance <= ~0x8000'0000ULL);
|
||||
}
|
||||
|
||||
void EmitX64::EmitCallHostFunction(EmitContext& ctx, IR::Inst* inst) {
|
||||
auto args = ctx.reg_alloc.GetArgumentInfo(inst);
|
||||
ctx.reg_alloc.HostCall(code, nullptr, args[1], args[2], args[3]);
|
||||
code.mov(rax, args[0].GetImmediateU64());
|
||||
code.call(rax);
|
||||
auto target = args[0].GetImmediateU64();
|
||||
if (IsWithin2G(uintptr_t(code.getCurr()), target)) {
|
||||
auto const f = std::bit_cast<void(*)(void)>(target);
|
||||
code.call(f);
|
||||
} else {
|
||||
code.mov(rax, target);
|
||||
code.call(rax);
|
||||
}
|
||||
}
|
||||
|
||||
void EmitX64::PushRSBHelper(Xbyak::Reg64 loc_desc_reg, Xbyak::Reg64 index_reg, IR::LocationDescriptor target) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user