ui: Redesign user interface

Introduces a new user interface that looks much nicer, is easier to
navigate with controllers, provides more context to users, and is
scalable. Some additional features are included.

* Adds 'popup menu' with actions that can be used easily from controller
* Adds 'main menu', unifying other configuration dialogs
* Adds port-forwarding user interface
* Adds screenshot feature
* Adds volume control feature
* Adds gamepad auto-bind option
* Adds vsync configuration option
* Adds auto UI scaling
* Adds preferred window size selection
* Adds AV pack selection
* Exposes some existing config items in GUI
This commit is contained in:
Matt Borgerson 2022-05-03 03:07:22 -07:00 committed by mborgerson
parent 306891b98c
commit 9c06980275
98 changed files with 12880 additions and 3846 deletions

View file

@ -68,6 +68,7 @@ IncludeCategories:
IncludeIsMainRegex: '$'
IndentCaseLabels: false
IndentWidth: 4
AccessModifierOffset: -4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: '.*_BEGIN$' # only PREC_BEGIN ?

8
.gitmodules vendored
View file

@ -64,12 +64,12 @@
[submodule "roms/vbootrom"]
path = roms/vbootrom
url = https://gitlab.com/qemu-project/vbootrom.git
[submodule "ui/imgui"]
path = ui/imgui
[submodule "ui/thirdparty/imgui"]
path = ui/thirdparty/imgui
url = https://github.com/ocornut/imgui.git
ignore = untracked
[submodule "ui/implot"]
path = ui/implot
[submodule "ui/thirdparty/implot"]
path = ui/thirdparty/implot
url = https://github.com/epezent/implot.git
ignore = untracked
[submodule "hw/xbox/nv2a/xxHash"]

View file

@ -272,11 +272,6 @@ set -x # Print commands from now on
${opts} \
"$@"
# Force imgui update now to work around annoying make issue
if ! test -f "${project_source_dir}/ui/imgui/imgui.cpp"; then
./scripts/git-submodule.sh update ui/imgui
fi
time make -j"${job_count}" ${target} 2>&1 | tee build.log
"${postbuild}" # call post build functions

View file

@ -6,8 +6,10 @@ general:
check:
type: bool
default: true
misc:
skip_boot_anim: bool
screenshot_dir: string
skip_boot_anim: bool
# throttle_io: bool
last_viewed_menu_index: integer
user_token: string
input:
@ -16,6 +18,11 @@ input:
port2: string
port3: string
port4: string
gamecontrollerdb_path: string
auto_bind:
type: bool
default: true
background_input_capture: bool
display:
quality:
@ -23,9 +30,27 @@ display:
type: integer
default: 1
window:
last_width: integer
last_height: integer
fullscreen_on_startup: bool
startup_size:
type: enum
values: [last_used, 640x480, 1280x720, 1280x800, 1280x960, 1920x1080, 2560x1440, 2560x1600, 2560x1920, 3840x2160]
default: 1280x960
last_width:
type: integer
default: 640
last_height:
type: integer
default: 480
vsync:
type: bool
default: true
ui:
show_menubar:
type: bool
default: true
use_animations:
type: bool
default: true
fit:
type: enum
values: [center, scale, scale_16_9, scale_4_3, stretch]
@ -33,9 +58,15 @@ display:
scale:
type: integer
default: 1
auto_scale:
type: bool
default: true
audio:
use_dsp: bool
volume_limit:
type: number
default: 1
net:
enable: bool
@ -52,12 +83,26 @@ net:
remote_addr:
type: string
default: 1.2.3.4:9368
nat:
forward_ports:
type: array
items:
host: integer
guest: integer
protocol:
type: enum
values: [tcp, udp]
default: tcp
sys:
mem_limit:
type: enum
values: ['64', '128']
default: '64'
avpack:
type: enum
values: [scart, hdtv, vga, rfu, svideo, composite, none]
default: hdtv
files:
bootrom_path: string
flashrom_path: string
@ -69,3 +114,6 @@ perf:
hard_fpu:
type: bool
default: true
# cache_shaders:
# type: bool
# default: true

2
configure vendored
View file

@ -261,7 +261,7 @@ else
git_submodules_action="ignore"
fi
git_submodules="ui/keycodemapdb ui/imgui ui/implot util/xxHash"
git_submodules="ui/keycodemapdb ui/thirdparty/imgui ui/thirdparty/implot util/xxHash tomlplusplus genconfig"
git="git"
# Don't accept a target_list environment variable.

Binary file not shown.

156
data/abxy.svg Normal file
View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="76.111221mm"
height="9.6190205mm"
viewBox="0 0 76.111221 9.6190205"
version="1.1"
id="svg986"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="abxy.svg">
<defs
id="defs980" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="276.28204"
inkscape:cy="77.212294"
inkscape:document-units="mm"
inkscape:current-layer="g1622"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1043"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata983">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-48.12296,-137.22026)"
style="display:none">
<circle
style="opacity:0.9;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
id="path1694"
cx="63.995201"
cy="141.96429"
r="4.6772165" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.3499999px;line-height:1.25;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="62.234642"
y="144.12724"
id="text1698"><tspan
sodipodi:role="line"
id="tspan1696"
x="62.234642"
y="144.12724"
style="stroke-width:0.26458332">A</tspan></text>
<circle
r="4.6772165"
cy="141.96429"
cx="79.870201"
id="circle1580"
style="opacity:0.9;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1" />
<text
id="text1584"
y="144.12724"
x="78.109634"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.3499999px;line-height:1.25;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="stroke-width:0.26458332"
y="144.12724"
x="78.109634"
id="tspan1582"
sodipodi:role="line">B</tspan></text>
<circle
style="opacity:0.9;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
id="circle1586"
cx="92.041039"
cy="141.96429"
r="4.6772165" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.3499999px;line-height:1.25;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="90.280449"
y="144.12724"
id="text1590"><tspan
sodipodi:role="line"
id="tspan1588"
x="90.280449"
y="144.12724"
style="stroke-width:0.26458332">X</tspan></text>
<circle
r="4.6772165"
cy="141.96429"
cx="104.74105"
id="circle1592"
style="opacity:0.9;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1" />
<text
id="text1596"
y="144.12724"
x="102.98046"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.3499999px;line-height:1.25;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="stroke-width:0.26458332"
y="144.12724"
x="102.98046"
id="tspan1594"
sodipodi:role="line">Y</tspan></text>
</g>
<g
transform="translate(-48.12296,-137.22026)"
id="g1622"
inkscape:groupmode="layer"
inkscape:label="Layer 1 copy">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
d="m 52.800194,137.28692 a 4.6772165,4.6772165 0 0 0 -4.677234,4.67724 4.6772165,4.6772165 0 0 0 4.677234,4.67723 4.6772165,4.6772165 0 0 0 4.677234,-4.67723 4.6772165,4.6772165 0 0 0 -4.677234,-4.67724 z m -0.173116,2.32596 h 0.486792 l 1.457275,4.51445 h -0.58291 l -0.356567,-1.17822 H 52.10618 l -0.350367,1.17822 h -0.58291 z m 0.241846,0.79375 -0.613916,2.05259 h 1.230932 z"
id="circle1598"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
d="m 66.382135,137.28692 a 4.6772165,4.6772165 0 0 0 -4.677233,4.67724 4.6772165,4.6772165 0 0 0 4.677233,4.67723 4.6772165,4.6772165 0 0 0 4.677235,-4.67723 4.6772165,4.6772165 0 0 0 -4.677235,-4.67724 z m -1.273823,2.32596 h 1.286742 c 0.411345,0 0.720371,0.10025 0.927076,0.30076 0.208773,0.2005 0.313159,0.50126 0.313159,0.90227 0,0.21084 -0.05271,0.39687 -0.158129,0.5581 -0.10542,0.16123 -0.248046,0.28629 -0.427881,0.37517 0.206706,0.062 0.370004,0.18914 0.489892,0.38137 0.121957,0.19224 0.182935,0.42375 0.182935,0.69454 0,0.40514 -0.111622,0.72347 -0.334864,0.95498 -0.221173,0.23151 -0.537435,0.34726 -0.94878,0.34726 h -1.33015 z m 0.567406,0.48989 v 1.43557 h 0.728639 c 0.196371,0 0.354499,-0.0661 0.47439,-0.19844 0.121954,-0.13229 0.182933,-0.30799 0.182933,-0.52709 0,-0.24805 -0.05478,-0.42789 -0.16433,-0.53951 -0.109553,-0.11368 -0.276987,-0.17053 -0.502296,-0.17053 z m 0,1.91306 v 1.62471 h 0.775148 c 0.214973,0 0.385505,-0.0703 0.511595,-0.21084 0.126093,-0.14263 0.189138,-0.3421 0.189138,-0.59841 0,-0.54364 -0.229444,-0.81546 -0.68833,-0.81546 z"
id="circle1604"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
d="m 79.964081,137.28692 a 4.6772165,4.6772165 0 0 0 -4.677233,4.67724 4.6772165,4.6772165 0 0 0 4.677233,4.67723 4.6772165,4.6772165 0 0 0 4.677235,-4.67723 4.6772165,4.6772165 0 0 0 -4.677235,-4.67724 z m -1.524971,2.32596 h 0.663527 l 0.85576,1.73013 0.849562,-1.73013 h 0.666625 l -1.175123,2.23862 1.199928,2.27583 h -0.672828 l -0.868164,-1.76113 -0.871265,1.76113 h -0.672827 l 1.20613,-2.27583 z"
id="circle1610"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
d="m 93.546026,137.28692 a 4.6772165,4.6772165 0 0 0 -4.677233,4.67724 4.6772165,4.6772165 0 0 0 4.677233,4.67723 4.6772165,4.6772165 0 0 0 4.677235,-4.67723 4.6772165,4.6772165 0 0 0 -4.677235,-4.67724 z m -1.667598,2.32596 h 0.644922 l 0.923976,2.26653 0.923975,-2.26653 h 0.641821 l -1.283645,2.83083 v 1.68362 H 93.16207 v -1.68362 z"
id="circle1616"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
data/abxy.ttf Normal file

Binary file not shown.

161
data/abxy_font.svg Normal file
View file

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="1000"
width="1000"
sodipodi:docname="abxy_font.svg"
version="1.1"
id="svg1688"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="580.56811"
inkscape:cy="584.04581"
inkscape:document-units="px"
inkscape:current-layer="layer2"
showgrid="false"
showguides="true"
inkscape:window-width="1727"
inkscape:window-height="1380"
inkscape:window-x="3633"
inkscape:window-y="576"
inkscape:window-maximized="0"
showborder="true"
inkscape:showpageshadow="false">
<sodipodi:guide
id="guide_baseline"
inkscape:label="baseline"
position="0,253"
orientation="0,1"
inkscape:locked="false" />
<sodipodi:guide
id="guide_ascender"
inkscape:label="ascender"
position="0,945"
orientation="0,1"
inkscape:locked="false" />
<sodipodi:guide
id="guide_caps"
inkscape:label="caps"
position="0,896"
orientation="0,1"
inkscape:locked="false" />
<sodipodi:guide
id="guide_xheight"
inkscape:label="xheight"
position="0,729"
orientation="0,1"
inkscape:locked="false" />
<sodipodi:guide
id="guide_descender"
inkscape:label="descender"
position="0,28"
orientation="0,1"
inkscape:locked="false" />
</sodipodi:namedview>
<defs
id="defs4">
<font
horiz-adv-x="1024"
id="font2233"
inkscape:label="font 1">
<font-face
units-per-em="1024"
id="font-face2235"
font-family="ABXY" />
<missing-glyph
d="M0,0h1000v1024h-1000z"
id="missing-glyph2237" />
<glyph
glyph-name="Button_A"
id="glyph2313"
unicode="A"
d="M 500.00007,752.85352 A 237.17679,237.17679 0 0 1 262.82246,515.67551 237.17679,237.17679 0 0 1 500.00007,278.49804 237.17679,237.17679 0 0 1 737.17754,515.67551 237.17679,237.17679 0 0 1 500.00007,752.85352 Z M 491.2215,634.90651 h 24.68493 L 589.80332,405.98342 H 560.24448 L 542.1634,465.7298 h -77.35585 l -17.76686,-59.74638 h -29.5587 z m 12.26384,-40.25024 -31.13114,-104.0847 h 62.41953 z" />
<glyph
glyph-name="Button_B"
id="glyph2315"
d="M 500,752.85352 A 237.17679,237.17679 0 0 1 262.82239,515.67551 237.17679,237.17679 0 0 1 500,278.49804 237.17679,237.17679 0 0 1 737.17761,515.67551 237.17679,237.17679 0 0 1 500,752.85352 Z M 435.40562,634.90651 h 65.24952 c 20.85888,0 36.5291,-5.08361 47.01093,-15.25121 10.58662,-10.16722 15.88007,-25.41843 15.88007,-45.75326 0,-10.6914 -2.67302,-20.12485 -8.01866,-28.30062 -5.34563,-8.17576 -12.57819,-14.51744 -21.69729,-19.02454 10.48183,-3.14394 18.76237,-9.59108 24.84178,-19.33875 6.18444,-9.74834 9.2766,-21.48799 9.2766,-35.2195 0,-20.54426 -5.66026,-36.68635 -16.98063,-48.426 -11.21546,-11.73965 -27.25277,-17.60921 -48.11165,-17.60921 h -67.45054 z m 28.77261,-24.84178 v -72.79631 h 36.94851 c 9.95777,0 17.97629,3.3519 24.05582,10.0627 6.18432,6.70824 9.27633,15.6179 9.27633,26.72816 0,12.57833 -2.7778,21.69783 -8.33287,27.35795 -5.55546,5.76464 -14.04572,8.6475 -25.47088,8.6475 z m 0,-97.00937 v -82.38727 h 39.30691 c 10.9011,0 19.5486,3.56483 25.94248,10.6914 6.39402,7.23269 9.59109,17.34758 9.59109,30.34479 0,27.56738 -11.635,41.35108 -34.90459,41.35108 z"
unicode="B" />
<glyph
glyph-name="Button_X"
id="glyph2317"
d="M 500,752.85352 A 237.17679,237.17679 0 0 1 262.82226,515.67551 237.17679,237.17679 0 0 1 500,278.49804 237.17679,237.17679 0 0 1 737.17774,515.67551 237.17679,237.17679 0 0 1 500,752.85352 Z M 422.67044,634.90651 h 33.64651 l 43.39486,-87.73318 43.0805,87.73318 h 33.80376 L 517.0068,521.38836 577.85388,405.98342 h -34.11837 l -44.0237,89.30508 -44.18095,-89.30508 h -34.11837 l 61.16157,115.40494 z"
unicode="X" />
<glyph
glyph-name="Button_Y"
id="glyph2319"
d="M 500,752.85352 A 237.17679,237.17679 0 0 1 262.82239,515.67551 237.17679,237.17679 0 0 1 500,278.49804 237.17679,237.17679 0 0 1 737.17761,515.67551 237.17679,237.17679 0 0 1 500,752.85352 Z M 415.43788,634.90651 h 32.70331 l 46.85369,-114.93335 46.85382,114.93335 h 32.54622 L 509.3025,491.35806 v -85.37464 h -28.77261 v 85.37464 z"
unicode="Y" />
</font>
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="A"
style="display:none">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:13.41674423;stroke-opacity:1"
d="M 500.00007,271.14648 A 237.17679,237.17679 0 0 0 262.82246,508.32449 237.17679,237.17679 0 0 0 500.00007,745.50196 237.17679,237.17679 0 0 0 737.17754,508.32449 237.17679,237.17679 0 0 0 500.00007,271.14648 Z m -8.77857,117.94701 h 24.68493 l 73.89689,228.92309 H 560.24448 L 542.1634,558.2702 h -77.35585 l -17.76686,59.74638 h -29.5587 z m 12.26384,40.25024 -31.13114,104.0847 h 62.41953 z"
id="circle1598"
inkscape:connector-curvature="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="B"
style="display:none">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:13.41674423;stroke-opacity:1"
d="M 500,271.14648 A 237.17679,237.17679 0 0 0 262.82239,508.32449 237.17679,237.17679 0 0 0 500,745.50196 237.17679,237.17679 0 0 0 737.17761,508.32449 237.17679,237.17679 0 0 0 500,271.14648 Z m -64.59438,117.94701 h 65.24952 c 20.85888,0 36.5291,5.08361 47.01093,15.25121 10.58662,10.16722 15.88007,25.41843 15.88007,45.75326 0,10.6914 -2.67302,20.12485 -8.01866,28.30062 -5.34563,8.17576 -12.57819,14.51744 -21.69729,19.02454 10.48183,3.14394 18.76237,9.59108 24.84178,19.33875 6.18444,9.74834 9.2766,21.48799 9.2766,35.2195 0,20.54426 -5.66026,36.68635 -16.98063,48.426 -11.21546,11.73965 -27.25277,17.60921 -48.11165,17.60921 h -67.45054 z m 28.77261,24.84178 v 72.79631 h 36.94851 c 9.95777,0 17.97629,-3.3519 24.05582,-10.0627 6.18432,-6.70824 9.27633,-15.6179 9.27633,-26.72816 0,-12.57833 -2.7778,-21.69783 -8.33287,-27.35795 -5.55546,-5.76464 -14.04572,-8.6475 -25.47088,-8.6475 z m 0,97.00937 v 82.38727 h 39.30691 c 10.9011,0 19.5486,-3.56483 25.94248,-10.6914 6.39402,-7.23269 9.59109,-17.34758 9.59109,-30.34479 0,-27.56738 -11.635,-41.35108 -34.90459,-41.35108 z"
id="circle1604"
inkscape:connector-curvature="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="X"
style="display:none">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:13.41674423;stroke-opacity:1"
d="M 500,271.14648 A 237.17679,237.17679 0 0 0 262.82226,508.32449 237.17679,237.17679 0 0 0 500,745.50196 237.17679,237.17679 0 0 0 737.17774,508.32449 237.17679,237.17679 0 0 0 500,271.14648 Z m -77.32956,117.94701 h 33.64651 l 43.39486,87.73318 43.0805,-87.73318 h 33.80376 l -59.58927,113.51815 60.84708,115.40494 h -34.11837 l -44.0237,-89.30508 -44.18095,89.30508 h -34.11837 l 61.16157,-115.40494 z"
id="circle1610"
inkscape:connector-curvature="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Y"
style="display:inline">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:13.41674423;stroke-opacity:1"
d="M 500,271.14648 A 237.17679,237.17679 0 0 0 262.82239,508.32449 237.17679,237.17679 0 0 0 500,745.50196 237.17679,237.17679 0 0 0 737.17761,508.32449 237.17679,237.17679 0 0 0 500,271.14648 Z m -84.56212,117.94701 h 32.70331 l 46.85369,114.93335 46.85382,-114.93335 h 32.54622 L 509.3025,532.64194 v 85.37464 h -28.77261 v -85.37464 z"
id="circle1616"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

View file

@ -2,7 +2,10 @@ pfiles = [
'controller_mask.png',
'logo_sdf.png',
'xemu_64x64.png',
'roboto_medium.ttf',
'abxy.ttf',
'Roboto-Medium.ttf',
'RobotoCondensed-Regular.ttf',
'font_awesome_6_1_1_solid.otf',
]
libpfile_targets = []

@ -1 +1 @@
Subproject commit 8220e8748922ddd9bba4cbacd608601586c9a4bb
Subproject commit 5da3fd2463288d9e048dbf3ea41f2bad0a4287a8

View file

@ -297,6 +297,27 @@ void mcpx_apu_debug_toggle_mute(uint16_t v)
g_dbg_muted_voices[v / 64] ^= (1LL << (v % 64));
}
static void mcpx_apu_update_dsp_preference(MCPXAPUState *d)
{
static int last_known_preference = -1;
if (last_known_preference == (int)g_config.audio.use_dsp) {
return;
}
if (g_config.audio.use_dsp) {
d->mon = MCPX_APU_DEBUG_MON_GP_OR_EP;
d->gp.realtime = true;
d->ep.realtime = true;
} else {
d->mon = MCPX_APU_DEBUG_MON_VP;
d->gp.realtime = false;
d->ep.realtime = false;
}
last_known_preference = g_config.audio.use_dsp;
}
static float clampf(float v, float min, float max)
{
if (v < min) {
@ -2050,6 +2071,7 @@ static int voice_get_samples(MCPXAPUState *d, uint32_t v, float samples[][2],
static void se_frame(MCPXAPUState *d)
{
mcpx_apu_update_dsp_preference(d);
mcpx_debug_begin_frame();
g_dbg.gp_realtime = d->gp.realtime;
g_dbg.ep_realtime = d->ep.realtime;
@ -2137,6 +2159,7 @@ static void se_frame(MCPXAPUState *d)
d->apu_fifo_output[off + i][0] += isamp[2*i];
d->apu_fifo_output[off + i][1] += isamp[2*i+1];
}
memset(d->vp.sample_buf, 0, sizeof(d->vp.sample_buf));
memset(mixbins, 0, sizeof(mixbins));
}
@ -2198,6 +2221,15 @@ static void se_frame(MCPXAPUState *d)
fwrite(d->apu_fifo_output, sizeof(d->apu_fifo_output), 1, fd);
fclose(fd);
#endif
if (0 <= g_config.audio.volume_limit && g_config.audio.volume_limit < 1) {
float f = pow(g_config.audio.volume_limit, M_E);
for (int i = 0; i < 256; i++) {
d->apu_fifo_output[i][0] *= f;
d->apu_fifo_output[i][1] *= f;
}
}
qemu_spin_lock(&d->vp.out_buf_lock);
int num_bytes_free = fifo8_num_free(&d->vp.out_buf);
assert(num_bytes_free >= sizeof(d->apu_fifo_output));
@ -2624,15 +2656,7 @@ void mcpx_apu_init(PCIBus *bus, int devfn, MemoryRegion *ram)
/* Until DSP is more performant, a switch to decide whether or not we should
* use the full audio pipeline or not.
*/
if (g_config.audio.use_dsp) {
d->mon = MCPX_APU_DEBUG_MON_GP_OR_EP;
d->gp.realtime = true;
d->ep.realtime = true;
} else {
d->mon = MCPX_APU_DEBUG_MON_VP;
d->gp.realtime = false;
d->ep.realtime = false;
}
mcpx_apu_update_dsp_preference(d);
qemu_thread_create(&d->apu_thread, "mcpx.apu_thread", mcpx_apu_frame_thread,
d, QEMU_THREAD_JOINABLE);

View file

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2022 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

24
licenses/fpng.license.txt Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View file

@ -0,0 +1,16 @@
MIT License
Copyright (c) Mark Gillard <mark.gillard@outlook.com.au>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -641,6 +641,15 @@ static SlirpState *slirp_lookup(Monitor *mon, const char *id)
}
}
#ifdef XBOX
void *slirp_get_state_from_netdev(const char *id)
{
SlirpState *s = slirp_lookup(NULL, id);
if (!s) return NULL;
return s->slirp;
}
#endif
void hmp_hostfwd_remove(Monitor *mon, const QDict *qdict)
{
struct in_addr host_addr = { .s_addr = INADDR_ANY };

View file

@ -28,7 +28,7 @@ sub_file="${sub_tdir}/submodule.tar"
# different to the host OS.
submodules="dtc slirp meson ui/keycodemapdb"
submodules="$submodules tests/fp/berkeley-softfloat-3 tests/fp/berkeley-testfloat-3"
submodules="$submodules ui/imgui ui/implot util/xxHash tomlplusplus genconfig" # xemu extras
submodules="$submodules ui/thirdparty/imgui ui/thirdparty/implot util/xxHash tomlplusplus genconfig" # xemu extras
sub_deinit=""
function cleanup() {

View file

@ -20,6 +20,7 @@
zlib = 'zlib'
lgplv2_1 = 'lgplv2_1'
apache2 = 'apache2'
unlicense = 'unlicense'
multi = 'multi'
@ -179,13 +180,13 @@ def head(self):
Lib('imgui', 'https://github.com/ocornut/imgui',
mit, 'https://raw.githubusercontent.com/ocornut/imgui/master/LICENSE.txt',
ships_static=all_platforms,
submodule=Submodule('ui/imgui')
submodule=Submodule('ui/thirdparty/imgui')
),
Lib('implot', 'https://github.com/epezent/implot',
mit, 'https://raw.githubusercontent.com/epezent/implot/master/LICENSE',
ships_static=all_platforms,
submodule=Submodule('ui/implot')
submodule=Submodule('ui/thirdparty/implot')
),
Lib('httplib', 'https://github.com/yhirose/cpp-httplib',
@ -206,10 +207,10 @@ def head(self):
version='2.25'
),
Lib('inih', 'https://github.com/benhoyt/inih',
bsd, 'https://raw.githubusercontent.com/mborgerson/xemu/master/ui/inih/LICENSE.txt',
Lib('tomlplusplus', 'https://github.com/marzer/tomlplusplus',
mit, 'https://raw.githubusercontent.com/marzer/tomlplusplus/master/LICENSE',
ships_static=all_platforms,
version='351217124ddb3e3fe2b982248a04c672350bb0af'
submodule=Submodule('tomlplusplus')
),
Lib('xxHash', 'https://github.com/Cyan4973/xxHash.git',
@ -218,6 +219,12 @@ def head(self):
submodule=Submodule('util/xxHash')
),
Lib('fpng', 'https://github.com/richgel999/fpng',
unlicense, 'https://github.com/richgel999/fpng/blob/main/README.md',
ships_static=all_platforms,
version='6926f5a0a78f22d42b074a0ab8032e07736babd4'
),
#
# Data files included with xemu
#
@ -228,6 +235,12 @@ def head(self):
version='2.138'
),
Lib('fontawesome', 'https://fontawesome.com',
multi, '',
ships_static=all_platforms,
version='6.1.1'
),
#
# Libraries either linked statically, dynamically linked & shipped, or dynamically linked with system-installed libraries only
#

View file

@ -2866,10 +2866,21 @@ void qemu_init(int argc, char **argv, char **envp)
}
}
fake_argv[fake_argc++] = g_strdup_printf("xbox%s%s%s",
const char *avpack_str = (const char *[]){
"scart",
"hdtv",
"vga",
"rfu",
"svideo",
"composite",
"none",
}[g_config.sys.avpack];
fake_argv[fake_argc++] = g_strdup_printf("xbox%s%s%s,avpack=%s",
(bootrom_arg != NULL) ? bootrom_arg : "",
g_config.general.misc.skip_boot_anim ? ",short-animation=on" : "",
",kernel-irqchip=off"
g_config.general.skip_boot_anim ? ",short-animation=on" : "",
",kernel-irqchip=off",
avpack_str
);
if (bootrom_arg != NULL) {

@ -1 +0,0 @@
Subproject commit e18abe3619cfa0eced163c027d0349506814816c

@ -1 +0,0 @@
Subproject commit a6bab98517b1baa3116db52518dda1eb2d7eaab7

View file

@ -15,57 +15,35 @@ softmmu_ss.add(files(
'udmabuf.c',
))
imgui_files = files(
'imgui/imgui.cpp',
'imgui/imgui_draw.cpp',
'imgui/imgui_tables.cpp',
#'imgui/imgui_demo.cpp',
'imgui/imgui_widgets.cpp',
'imgui/backends/imgui_impl_opengl3.cpp',
'imgui/backends/imgui_impl_sdl.cpp',
'implot/implot.cpp',
#'implot/implot_demo.cpp',
'implot/implot_items.cpp'
)
imgui_cppargs = ['-DIMGUI_IMPL_OPENGL_LOADER_CUSTOM="epoxy/gl.h"']
libimgui = static_library('imgui',
sources: imgui_files,
cpp_args: imgui_cppargs,
include_directories: 'imgui',
dependencies: [sdl, opengl])
imgui = declare_dependency(link_with: libimgui,
compile_args: imgui_cppargs,
include_directories: 'imgui')
subdir('thirdparty')
xemu_ss = ss.source_set()
xemu_ss.add(files(
'xemu.c',
'xemu-custom-widgets.c',
'xemu-data.c',
'xemu-input.c',
'xemu-monitor.c',
'xemu-net.c',
'xemu-settings.cc',
'xemu-shaders.c',
'xemu-hud.cc',
'xemu-reporting.cc',
'xemu.c',
'xemu-data.c',
))
xemu_ss.add(when: 'CONFIG_WIN32', if_true: files('xemu-update.cc'))
subdir('xui')
if 'CONFIG_DARWIN' in config_host
xemu_cocoa = dependency('appleframeworks', modules: 'Cocoa')
xemu_ss.add(xemu_cocoa) # FIXME: Use existing cocoa name
xemu_ss.add(xemu_cocoa)
endif
xemu_ss.add(imgui, sdl, opengl, openssl)
xemu_ss.add(when: 'CONFIG_LINUX', if_true: [xemu_gtk, files('xemu-os-utils-linux.c', 'noc_file_dialog_gtk.c')])
xemu_ss.add(when: 'CONFIG_WIN32', if_true: files('xemu-os-utils-windows.c', 'noc_file_dialog_win32.c'))
xemu_ss.add(when: 'CONFIG_DARWIN', if_true: files('xemu-os-utils-macos.m', 'noc_file_dialog_macos.m'))
if 'CONFIG_LINUX' in config_host
xemu_ss.add(xemu_gtk)
endif
xemu_ss.add(when: 'CONFIG_LINUX', if_true: [xemu_gtk, files('xemu-os-utils-linux.c')])
xemu_ss.add(when: 'CONFIG_WIN32', if_true: files('xemu-os-utils-windows.c'))
xemu_ss.add(when: 'CONFIG_DARWIN', if_true: files('xemu-os-utils-macos.m'))
xemu_ss.add(when: 'CONFIG_RENDERDOC', if_true: [libdl])
xemu_ss.add(imgui, implot, stb_image, noc, sdl, opengl, openssl, fa, fpng, json, httplib)
softmmu_ss.add_all(xemu_ss)

1393
ui/thirdparty/fa/IconsFontAwesome6.h vendored Normal file

File diff suppressed because it is too large Load diff

1
ui/thirdparty/fpng/HEAD vendored Normal file
View file

@ -0,0 +1 @@
645d49cf6b2e82ce25b5b59f6a2e2df30e6f5fa6

3222
ui/thirdparty/fpng/fpng.cpp vendored Normal file

File diff suppressed because it is too large Load diff

122
ui/thirdparty/fpng/fpng.h vendored Normal file
View file

@ -0,0 +1,122 @@
// fpng.h - unlicense (see end of fpng.cpp)
#pragma once
#include <stdlib.h>
#include <stdint.h>
#include <vector>
#ifndef FPNG_TRAIN_HUFFMAN_TABLES
// Set to 1 when using the -t (training) option in fpng_test to generate new opaque/alpha Huffman tables for the single pass encoder.
#define FPNG_TRAIN_HUFFMAN_TABLES (0)
#endif
namespace fpng
{
// ---- Library initialization - call once to identify if the processor supports SSE.
// Otherwise you'll only get scalar fallbacks.
void fpng_init();
// ---- Useful Utilities
// Returns true if the CPU supports SSE 4.1, and SSE support wasn't disabled by setting FPNG_NO_SSE=1.
// fpng_init() must have been called first, or it'll assert and return false.
bool fpng_cpu_supports_sse41();
// Fast CRC-32 SSE4.1+pclmul or a scalar fallback (slice by 4)
const uint32_t FPNG_CRC32_INIT = 0;
uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32 = FPNG_CRC32_INIT);
// Fast Adler32 SSE4.1 Adler-32 with a scalar fallback.
const uint32_t FPNG_ADLER32_INIT = 1;
uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler = FPNG_ADLER32_INIT);
// ---- Compression
enum
{
// Enables computing custom Huffman tables for each file, instead of using the custom global tables.
// Results in roughly 6% smaller files on average, but compression is around 40% slower.
FPNG_ENCODE_SLOWER = 1,
// Only use raw Deflate blocks (no compression at all). Intended for testing.
FPNG_FORCE_UNCOMPRESSED = 2,
};
// Fast PNG encoding. The resulting file can be decoded either using a standard PNG decoder or by the fpng_decode_memory() function below.
// pImage: pointer to RGB or RGBA image pixels, R first in memory, B/A last.
// w/h - image dimensions. Image's row pitch in bytes must is w*num_chans.
// num_chans must be 3 or 4.
bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector<uint8_t>& out_buf, uint32_t flags = 0);
#ifndef FPNG_NO_STDIO
// Fast PNG encoding to the specified file.
bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags = 0);
#endif
// ---- Decompression
enum
{
FPNG_DECODE_SUCCESS = 0, // file is a valid PNG file and written by FPNG and the decode succeeded
FPNG_DECODE_NOT_FPNG, // file is a valid PNG file, but it wasn't written by FPNG so you should try decoding it with a general purpose PNG decoder
FPNG_DECODE_INVALID_ARG, // invalid function parameter
FPNG_DECODE_FAILED_NOT_PNG, // file cannot be a PNG file
FPNG_DECODE_FAILED_HEADER_CRC32, // a chunk CRC32 check failed, file is likely corrupted or not PNG
FPNG_DECODE_FAILED_INVALID_DIMENSIONS, // invalid image dimensions in IHDR chunk (0 or too large)
FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE, // decoding the file fully into memory would likely require too much memory (only on 32bpp builds)
FPNG_DECODE_FAILED_CHUNK_PARSING, // failed while parsing the chunk headers, or file is corrupted
FPNG_DECODE_FAILED_INVALID_IDAT, // IDAT data length is too small and cannot be valid, file is either corrupted or it's a bug
// fpng_decode_file() specific errors
FPNG_DECODE_FILE_OPEN_FAILED,
FPNG_DECODE_FILE_TOO_LARGE,
FPNG_DECODE_FILE_READ_FAILED,
FPNG_DECODE_FILE_SEEK_FAILED
};
// Fast PNG decoding of files ONLY created by fpng_encode_image_to_memory() or fpng_encode_image_to_file().
// If fpng_get_info() or fpng_decode_memory() returns FPNG_DECODE_NOT_FPNG, you should decode the PNG by falling back to a general purpose decoder.
//
// fpng_get_info() parses the PNG header and iterates through all chunks to determine if it's a file written by FPNG, but does not decompress the actual image data so it's relatively fast.
//
// pImage, image_size: Pointer to PNG image data and its size
// width, height: output image's dimensions
// channels_in_file: will be 3 or 4
//
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file);
// fpng_decode_memory() decompresses 24/32bpp PNG files ONLY encoded by this module.
// If the image was written by FPNG, it will decompress the image data, otherwise it will return FPNG_DECODE_NOT_FPNG in which case you should fall back to a general purpose PNG decoder (lodepng, stb_image, libpng, etc.)
//
// pImage, image_size: Pointer to PNG image data and its size
// out: Output 24/32bpp image buffer
// width, height: output image's dimensions
// channels_in_file: will be 3 or 4
// desired_channels: must be 3 or 4
//
// If the image is 24bpp and 32bpp is requested, the alpha values will be set to 0xFF.
// If the image is 32bpp and 24bpp is requested, the alpha values will be discarded.
//
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
int fpng_decode_memory(const void* pImage, uint32_t image_size, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
#ifndef FPNG_NO_STDIO
int fpng_decode_file(const char* pFilename, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
#endif
// ---- Internal API used for Huffman table training purposes
#if FPNG_TRAIN_HUFFMAN_TABLES
const uint32_t HUFF_COUNTS_SIZE = 288;
extern uint64_t g_huff_counts[HUFF_COUNTS_SIZE];
bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector<uint8_t>& prefix, uint64_t& bit_buf, int& bit_buf_size, uint32_t *pCodes, uint8_t *pCodesizes);
#endif
} // namespace fpng

1
ui/thirdparty/imgui vendored Submodule

@ -0,0 +1 @@
Subproject commit c71a50deb5ddf1ea386b91e60fa2e4a26d080074

View file

@ -0,0 +1 @@
#include <epoxy/gl.h>

1
ui/thirdparty/implot vendored Submodule

@ -0,0 +1 @@
Subproject commit b47c8bacdbc78bc521691f70666f13924bb522ab

63
ui/thirdparty/meson.build vendored Normal file
View file

@ -0,0 +1,63 @@
imgui_files = files(
'imgui/imgui.cpp',
'imgui/imgui_draw.cpp',
'imgui/imgui_tables.cpp',
'imgui/imgui_widgets.cpp',
'imgui/backends/imgui_impl_sdl.cpp',
'imgui/backends/imgui_impl_opengl3.cpp',
#'imgui/imgui_demo.cpp',
)
imgui_cppargs = ['-DIMGUI_IMPL_OPENGL_LOADER_CUSTOM',
'-include', 'imgui_impl_opengl3_loader_override.h']
libimgui = static_library('imgui',
sources: imgui_files,
cpp_args: imgui_cppargs,
include_directories: ['.', 'imgui'],
dependencies: [sdl, opengl])
imgui = declare_dependency(link_with: libimgui,
include_directories: ['imgui', 'imgui/backends'])
implot_files = files(
'implot/implot.cpp',
'implot/implot_items.cpp'
#'implot/implot_demo.cpp',
)
libimplot = static_library('implot',
sources: implot_files,
include_directories: 'implot',
dependencies: [imgui])
implot = declare_dependency(link_with: libimplot,
include_directories: 'implot')
noc_ss = ss.source_set()
noc_ss.add(when: 'CONFIG_LINUX', if_true: [xemu_gtk, files('noc_file_dialog/noc_file_dialog_gtk.c')])
noc_ss.add(when: 'CONFIG_WIN32', if_true: files('noc_file_dialog/noc_file_dialog_win32.c'))
noc_ss.add(when: 'CONFIG_DARWIN', if_true: files('noc_file_dialog/noc_file_dialog_macos.m'))
noc_ss = noc_ss.apply(config_all, strict: false)
noclib = static_library('noc',
sources: noc_ss.sources(),
dependencies: noc_ss.dependencies(),
include_directories: 'noc_file_dialog')
noc = declare_dependency(include_directories: 'noc_file_dialog', link_with: noclib)
libstb_image = static_library('stb_image',
sources: 'stb_image/stb_image_impl.c')
stb_image = declare_dependency(include_directories: 'stb_image',
link_with: libstb_image)
fa = declare_dependency(include_directories: 'fa')
if cpu == 'x86_64'
libfpng_cpp_args = ['-DFPNG_NO_SSE=0', '-msse4.1', '-mpclmul']
else
libfpng_cpp_args = ['-DFPNG_NO_SSE=1']
endif
libfpng = static_library('fpng', sources: 'fpng/fpng.cpp', cpp_args: libfpng_cpp_args)
fpng = declare_dependency(include_directories: 'fpng', link_with: libfpng)
json = declare_dependency(include_directories: 'json')
httplib = declare_dependency(include_directories: 'httplib')

View file

@ -20,6 +20,8 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#ifndef NOC_FILE_DIALOG_H
#define NOC_FILE_DIALOG_H
/* A portable library to create open and save dialogs on linux, osx and
* windows.
@ -328,3 +330,4 @@ const char *noc_file_dialog_open(int flags,
#endif
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

View file

@ -1,311 +0,0 @@
/*
* xemu User Interface Rendering Helpers
*
* Copyright (C) 2020-2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <SDL.h>
#include <epoxy/gl.h>
#include <stdio.h>
#include <math.h>
#include "xemu-shaders.h"
#include "xemu-custom-widgets.h"
#include "data/controller_mask.png.h"
#include "data/logo_sdf.png.h"
static struct decal_shader *s = NULL;
static struct decal_shader *s_logo = NULL;
GLuint main_fb;
struct fbo *controller_fbo;
struct fbo *logo_fbo;
GLint vp[4];
GLuint g_ui_tex;
GLuint g_logo_tex;
struct rect {
int x, y, w, h;
};
const struct rect tex_items[] = {
{ 0, 148, 467, 364 }, // obj_controller
{ 0, 81, 67, 67 }, // obj_lstick
{ 0, 14, 67, 67 }, // obj_rstick
{ 67, 104, 68, 44 }, // obj_port_socket
{ 67, 76, 28, 28 }, // obj_port_lbl_1
{ 67, 48, 28, 28 }, // obj_port_lbl_2
{ 67, 20, 28, 28 }, // obj_port_lbl_3
{ 95, 76, 28, 28 }, // obj_port_lbl_4
};
enum tex_item_names {
obj_controller,
obj_lstick,
obj_rstick,
obj_port_socket,
obj_port_lbl_1,
obj_port_lbl_2,
obj_port_lbl_3,
obj_port_lbl_4,
};
void initialize_custom_ui_rendering(void)
{
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, (GLint*)&main_fb);
glGetIntegerv(GL_VIEWPORT, vp);
glActiveTexture(GL_TEXTURE0);
g_ui_tex = load_texture_from_memory(controller_mask_data, controller_mask_size);
s = create_decal_shader(SHADER_TYPE_MASK);
g_logo_tex = load_texture_from_memory(logo_sdf_data, logo_sdf_size);
s_logo = create_decal_shader(SHADER_TYPE_LOGO);
controller_fbo = create_fbo(512, 512);
logo_fbo = create_fbo(512, 512);
render_to_default_fb();
}
void render_meter(
struct decal_shader *s,
float x, float y, float width, float height, float p,
uint32_t color_bg, uint32_t color_fg)
{
render_decal(s, x, y, width, height,0, 0, 1, 1, 0, 0, color_bg);
render_decal(s, x, y, width*p, height ,0, 0, 1, 1, 0, 0, color_fg);
}
void render_controller(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, ControllerState *state)
{
// Location within the controller texture of masked button locations,
// relative to the origin of the controller
const struct rect jewel = { 177, 172, 113, 118 };
const struct rect lstick_ctr = { 93, 246, 0, 0 };
const struct rect rstick_ctr = { 342, 148, 0, 0 };
const struct rect buttons[12] = {
{ 367, 187, 30, 38 }, // A
{ 368, 229, 30, 38 }, // B
{ 330, 204, 30, 38 }, // X
{ 331, 247, 30, 38 }, // Y
{ 82, 121, 31, 47 }, // D-Left
{ 104, 160, 44, 25 }, // D-Up
{ 141, 121, 31, 47 }, // D-Right
{ 104, 105, 44, 25 }, // D-Down
{ 187, 94, 34, 24 }, // Back
{ 246, 94, 36, 26 }, // Start
{ 348, 288, 30, 38 }, // White
{ 386, 268, 30, 38 }, // Black
};
uint8_t alpha = 0;
uint32_t now = SDL_GetTicks();
float t;
glUseProgram(s->prog);
glBindVertexArray(s->vao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_ui_tex);
// Add a 5 pixel space around the controller so we can wiggle the controller
// around to visualize rumble in action
frame_x += 5;
frame_y += 5;
float original_frame_x = frame_x;
float original_frame_y = frame_y;
// Floating point versions that will get scaled
float rumble_l = 0;
float rumble_r = 0;
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ZERO);
uint32_t jewel_color = secondary_color;
// Check to see if the guide button is pressed
const uint32_t animate_guide_button_duration = 2000;
if (state->buttons & CONTROLLER_BUTTON_GUIDE) {
state->animate_guide_button_end = now + animate_guide_button_duration;
}
if (now < state->animate_guide_button_end) {
t = 1.0f - (float)(state->animate_guide_button_end-now)/(float)animate_guide_button_duration;
float sin_wav = (1-sin(M_PI * t / 2.0f));
// Animate guide button by highlighting logo jewel and fading out over time
alpha = sin_wav * 255.0f;
jewel_color = primary_color + alpha;
// Add a little extra flare: wiggle the frame around while we rumble
frame_x += ((float)(rand() % 5)-2.5) * (1-t);
frame_y += ((float)(rand() % 5)-2.5) * (1-t);
rumble_l = rumble_r = sin_wav;
}
// Render controller texture
render_decal(s,
frame_x+0, frame_y+0, tex_items[obj_controller].w, tex_items[obj_controller].h,
tex_items[obj_controller].x, tex_items[obj_controller].y, tex_items[obj_controller].w, tex_items[obj_controller].h,
primary_color, secondary_color, 0);
glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE); // Blend with controller cutouts
render_decal(s, frame_x+jewel.x, frame_y+jewel.y, jewel.w, jewel.h, 0, 0, 1, 1, 0, 0, jewel_color);
// The controller has alpha cutouts where the buttons are. Draw a surface
// behind the buttons if they are activated
for (int i = 0; i < 12; i++) {
bool enabled = !!(state->buttons & (1 << i));
if (!enabled) continue;
render_decal(s,
frame_x+buttons[i].x, frame_y+buttons[i].y,
buttons[i].w, buttons[i].h,
0, 0, 1, 1,
0, 0, (enabled ? primary_color : secondary_color)+0xff);
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Blend with controller
// Render left thumbstick
float w = tex_items[obj_lstick].w;
float h = tex_items[obj_lstick].h;
float c_x = frame_x+lstick_ctr.x;
float c_y = frame_y+lstick_ctr.y;
float lstick_x = (float)state->axis[CONTROLLER_AXIS_LSTICK_X]/32768.0;
float lstick_y = (float)state->axis[CONTROLLER_AXIS_LSTICK_Y]/32768.0;
render_decal(s,
(int)(c_x-w/2.0f+10.0f*lstick_x),
(int)(c_y-h/2.0f+10.0f*lstick_y),
w, h,
tex_items[obj_lstick].x, tex_items[obj_lstick].y, w, h,
(state->buttons & CONTROLLER_BUTTON_LSTICK) ? secondary_color : primary_color,
(state->buttons & CONTROLLER_BUTTON_LSTICK) ? primary_color : secondary_color,
0
);
// Render right thumbstick
w = tex_items[obj_rstick].w;
h = tex_items[obj_rstick].h;
c_x = frame_x+rstick_ctr.x;
c_y = frame_y+rstick_ctr.y;
float rstick_x = (float)state->axis[CONTROLLER_AXIS_RSTICK_X]/32768.0;
float rstick_y = (float)state->axis[CONTROLLER_AXIS_RSTICK_Y]/32768.0;
render_decal(s,
(int)(c_x-w/2.0f+10.0f*rstick_x),
(int)(c_y-h/2.0f+10.0f*rstick_y),
w, h,
tex_items[obj_rstick].x, tex_items[obj_rstick].y, w, h,
(state->buttons & CONTROLLER_BUTTON_RSTICK) ? secondary_color : primary_color,
(state->buttons & CONTROLLER_BUTTON_RSTICK) ? primary_color : secondary_color,
0
);
glBlendFunc(GL_ONE, GL_ZERO); // Don't blend, just overwrite values in buffer
// Render trigger bars
float ltrig = state->axis[CONTROLLER_AXIS_LTRIG] / 32767.0;
float rtrig = state->axis[CONTROLLER_AXIS_RTRIG] / 32767.0;
const uint32_t animate_trigger_duration = 1000;
if ((ltrig > 0) || (rtrig > 0)) {
state->animate_trigger_end = now + animate_trigger_duration;
rumble_l = fmax(rumble_l, ltrig);
rumble_r = fmax(rumble_r, rtrig);
}
// Animate trigger alpha down after a period of inactivity
alpha = 0x80;
if (state->animate_trigger_end > now) {
t = 1.0f - (float)(state->animate_trigger_end-now)/(float)animate_trigger_duration;
float sin_wav = (1-sin(M_PI * t / 2.0f));
alpha += fmin(sin_wav * 0x40, 0x80);
}
render_meter(s,
original_frame_x+10,
original_frame_y+tex_items[obj_controller].h+20,
150, 5,
ltrig,
primary_color + alpha,
primary_color + 0xff);
render_meter(s,
original_frame_x+tex_items[obj_controller].w-160,
original_frame_y+tex_items[obj_controller].h+20,
150, 5,
rtrig,
primary_color + alpha,
primary_color + 0xff);
// Apply rumble updates
state->rumble_l = (int)(rumble_l * (float)0xffff);
state->rumble_r = (int)(rumble_r * (float)0xffff);
xemu_input_update_rumble(state);
glBindVertexArray(0);
glUseProgram(0);
}
void render_controller_port(float frame_x, float frame_y, int i, uint32_t port_color)
{
glUseProgram(s->prog);
glBindVertexArray(s->vao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_ui_tex);
glBlendFunc(GL_ONE, GL_ZERO);
// Render port socket
render_decal(s,
frame_x, frame_y,
tex_items[obj_port_socket].w, tex_items[obj_port_socket].h,
tex_items[obj_port_socket].x, tex_items[obj_port_socket].y,
tex_items[obj_port_socket].w, tex_items[obj_port_socket].h,
port_color, port_color, 0
);
frame_x += (tex_items[obj_port_socket].w-tex_items[obj_port_lbl_1].w)/2;
frame_y += tex_items[obj_port_socket].h + 8;
// Render port label
render_decal(s,
frame_x, frame_y,
tex_items[obj_port_lbl_1+i].w, tex_items[obj_port_lbl_1+i].h,
tex_items[obj_port_lbl_1+i].x, tex_items[obj_port_lbl_1+i].y,
tex_items[obj_port_lbl_1+i].w, tex_items[obj_port_lbl_1+i].h,
port_color, port_color, 0
);
glBindVertexArray(0);
glUseProgram(0);
}
void render_logo(uint32_t time, uint32_t primary_color, uint32_t secondary_color, uint32_t fill_color)
{
s_logo->time = time;
glUseProgram(s_logo->prog);
glBindVertexArray(s->vao);
glBlendFunc(GL_ONE, GL_ZERO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_logo_tex);
render_decal(
s_logo,
0, 0, 512, 512,
0,
0,
128,
128,
primary_color, secondary_color, fill_color
);
glBindVertexArray(0);
glUseProgram(0);
}

View file

@ -1,45 +0,0 @@
/*
* xemu User Interface Rendering Helpers
*
* Copyright (C) 2020-2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef XEMU_CUSTOM_WIDGETS
#define XEMU_CUSTOM_WIDGETS
#include <stdint.h>
#include "xemu-input.h"
#include "xemu-shaders.h"
#ifdef __cplusplus
extern "C" {
#endif
// FIXME: Cleanup
extern struct fbo *controller_fbo;
extern struct fbo *logo_fbo;
void initialize_custom_ui_rendering(void);
void render_meter(struct decal_shader *s, float x, float y, float width, float height, float p, uint32_t color_bg, uint32_t color_fg);
void render_controller(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, ControllerState *state);
void render_controller_port(float frame_x, float frame_y, int i, uint32_t port_color);
void render_logo(uint32_t time, uint32_t primary_color, uint32_t secondary_color, uint32_t fill_color);
#ifdef __cplusplus
}
#endif
#endif

File diff suppressed because it is too large Load diff

View file

@ -96,7 +96,9 @@ static const char **port_index_to_settings_key_map[] = {
void xemu_input_init(void)
{
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
if (g_config.input.background_input_capture) {
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
}
if (SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) {
fprintf(stderr, "Failed to initialize SDL gamecontroller subsystem: %s\n", SDL_GetError());
@ -191,25 +193,42 @@ void xemu_input_process_sdl_events(const SDL_Event *event)
// upside in this case is that a person can use the same GUID on all
// ports and just needs to bind to the receiver and never needs to hit
// this dialog.
// Attempt to re-bind to port previously bound to
int port = 0;
while (1) {
bool did_bind = false;
while (!did_bind) {
port = xemu_input_get_controller_default_bind_port(new_con, port);
if (port < 0) {
// No (additional) default mappings
break;
}
if (xemu_input_get_bound(port) != NULL) {
// Something already bound here, try again for another port
} else if (!xemu_input_get_bound(port)) {
xemu_input_bind(port, new_con, 0);
did_bind = true;
break;
} else {
// Try again for another port
port++;
continue;
}
xemu_input_bind(port, new_con, 0);
}
// Try to bind to any open port, and if so remember the binding
if (!did_bind && g_config.input.auto_bind) {
for (port = 0; port < 4; port++) {
if (!xemu_input_get_bound(port)) {
xemu_input_bind(port, new_con, 1);
did_bind = true;
break;
}
}
}
if (did_bind) {
char buf[128];
snprintf(buf, sizeof(buf), "Connected '%s' to port %d", new_con->name, port+1);
xemu_queue_notification(buf);
break;
}
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
DPRINTF("Controller Removed: %d\n", event->cdevice.which);
int handled = 0;

View file

@ -33,6 +33,8 @@
#include "qemu/config-file.h"
#include "net/net.h"
#include "net/hub.h"
#include "net/slirp.h"
#include <libslirp.h>
#if defined(_WIN32)
#include <pcap/pcap.h>
#endif
@ -41,6 +43,8 @@
static const char *id = "xemu-netdev";
static const char *id_hubport = "xemu-netdev-hubport";
void *slirp_get_state_from_netdev(const char *id);
void xemu_net_enable(void)
{
Error *local_err = NULL;
@ -102,6 +106,38 @@ void xemu_net_enable(void)
// error_propagate(errp, local_err);
xemu_queue_error_message(error_get_pretty(local_err));
error_report_err(local_err);
return;
}
if (g_config.net.backend == CONFIG_NET_BACKEND_NAT) {
void *s = slirp_get_state_from_netdev(id);
assert(s != NULL);
struct in_addr host_addr = { .s_addr = INADDR_ANY };
struct in_addr guest_addr = { .s_addr = 0 };
inet_aton("10.0.2.15", &guest_addr);
for (int i = 0; i < g_config.net.nat.forward_ports_count; i++) {
bool is_udp = g_config.net.nat.forward_ports[i].protocol ==
CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_UDP;
int host_port = g_config.net.nat.forward_ports[i].host;
int guest_port = g_config.net.nat.forward_ports[i].guest;
if (slirp_add_hostfwd(s, is_udp, host_addr, host_port, guest_addr,
guest_port) < 0) {
error_setg(&local_err,
"Could not set host forwarding rule "
"%d -> %d (%s)\n",
host_port, guest_port, is_udp ? "udp" : "tcp");
xemu_queue_error_message(error_get_pretty(local_err));
break;
}
}
}
if (local_err) {
xemu_net_disable();
}
g_config.net.enable = true;
@ -124,13 +160,25 @@ static void remove_netdev(const char *name)
// error_setg(errp, "Device '%s' is not a netdev", name);
return;
}
qemu_opts_del(opts);
qemu_del_net_client(nc);
}
void xemu_net_disable(void)
{
if (g_config.net.backend == CONFIG_NET_BACKEND_NAT) {
void *s = slirp_get_state_from_netdev(id);
assert(s != NULL);
struct in_addr host_addr = { .s_addr = INADDR_ANY };
for (int i = 0; i < g_config.net.nat.forward_ports_count; i++) {
slirp_remove_hostfwd(s,
g_config.net.nat.forward_ports[i].protocol ==
CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_UDP,
host_addr,
g_config.net.nat.forward_ports[i].host);
}
}
remove_netdev(id);
remove_netdev(id_hubport);
g_config.net.enable = false;

View file

@ -25,9 +25,46 @@ extern "C" {
#endif
const char *xemu_get_os_info(void);
const char *xemu_get_cpu_info(void);
void xemu_open_web_browser(const char *url);
#ifndef _WIN32
#ifdef CONFIG_CPUID_H
#include <cpuid.h>
#endif
#endif
static inline const char *xemu_get_os_platform(void)
{
const char *platform_name;
#if defined(__linux__)
platform_name = "Linux";
#elif defined(_WIN32)
platform_name = "Windows";
#elif defined(__APPLE__)
platform_name = "macOS";
#else
platform_name = "Unknown";
#endif
return platform_name;
}
static inline const char *xemu_get_cpu_info(void)
{
const char *cpu_info = "";
#ifdef CONFIG_CPUID_H
static uint32_t brand[12];
if (__get_cpuid_max(0x80000004, NULL)) {
__get_cpuid(0x80000002, brand+0x0, brand+0x1, brand+0x2, brand+0x3);
__get_cpuid(0x80000003, brand+0x4, brand+0x5, brand+0x6, brand+0x7);
__get_cpuid(0x80000004, brand+0x8, brand+0x9, brand+0xa, brand+0xb);
}
cpu_info = (const char *)brand;
#endif
// FIXME: Support other architectures (e.g. ARM)
return cpu_info;
}
#ifdef __cplusplus
}
#endif

View file

@ -187,3 +187,31 @@ void xemu_settings_save(void)
fprintf(fd, "%s", config_tree.generate_delta_toml().c_str());
fclose(fd);
}
void add_net_nat_forward_ports(int host, int guest, CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol)
{
// FIXME: - Realloc the arrays instead of free/alloc
// - Don't need to copy as much
auto cnode = config_tree.child("net")
->child("nat")
->child("forward_ports");
cnode->update_from_struct(&g_config);
cnode->children.push_back(*cnode->array_item_type);
auto &e = cnode->children.back();
e.child("host")->set_integer(host);
e.child("guest")->set_integer(guest);
e.child("protocol")->set_enum_by_index(protocol);
cnode->free_allocations(&g_config);
cnode->store_to_struct(&g_config);
}
void remove_net_nat_forward_ports(unsigned int index)
{
auto cnode = config_tree.child("net")
->child("nat")
->child("forward_ports");
cnode->update_from_struct(&g_config);
cnode->children.erase(cnode->children.begin()+index);
cnode->free_allocations(&g_config);
cnode->store_to_struct(&g_config);
}

View file

@ -59,6 +59,9 @@ static inline void xemu_settings_set_string(const char **str, const char *new_st
*str = strdup(new_str);
}
void add_net_nat_forward_ports(int host, int guest, CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol);
void remove_net_nat_forward_ports(unsigned int index);
#ifdef __cplusplus
}
#endif

View file

@ -1,371 +0,0 @@
/*
* xemu User Interface Rendering Helpers
*
* Copyright (C) 2020-2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <SDL.h>
#include <epoxy/gl.h>
#include <stdio.h>
#include <math.h>
#include "xemu-shaders.h"
#include "ui/shader/xemu-logo-frag.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
GLuint compile_shader(GLenum type, const char *src)
{
GLint status;
char err_buf[512];
GLuint shader = glCreateShader(type);
if (shader == 0) {
fprintf(stderr, "ERROR: Failed to create shader\n");
return 0;
}
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
glGetShaderInfoLog(shader, sizeof(err_buf), NULL, err_buf);
fprintf(stderr, "ERROR: Shader compilation failed!\n\n");
fprintf(stderr, "[Shader Info Log]\n");
fprintf(stderr, "%s\n", err_buf);
fprintf(stderr, "[Shader Source]\n");
fprintf(stderr, "%s\n", src);
return 0;
}
return shader;
}
struct decal_shader *create_decal_shader(enum SHADER_TYPE type)
{
// Allocate shader wrapper object
struct decal_shader *s = (struct decal_shader *)malloc(sizeof(struct decal_shader));
assert(s != NULL);
s->flip = 0;
s->scale = 1.4;
s->smoothing = 1.0;
s->outline_dist = 1.0;
s->time = 0;
const char *vert_src =
"#version 150 core\n"
"uniform bool in_FlipY;\n"
"uniform vec4 in_ScaleOffset;\n"
"uniform vec4 in_TexScaleOffset;\n"
"in vec2 in_Position;\n"
"in vec2 in_Texcoord;\n"
"out vec2 Texcoord;\n"
"void main() {\n"
" vec2 t = in_Texcoord;\n"
" if (in_FlipY) t.y = 1-t.y;\n"
" Texcoord = t*in_TexScaleOffset.xy + in_TexScaleOffset.zw;\n"
" gl_Position = vec4(in_Position*in_ScaleOffset.xy+in_ScaleOffset.zw, 0.0, 1.0);\n"
"}\n";
GLuint vert = compile_shader(GL_VERTEX_SHADER, vert_src);
assert(vert != 0);
const char *image_frag_src =
"#version 150 core\n"
"uniform sampler2D tex;\n"
"in vec2 Texcoord;\n"
"out vec4 out_Color;\n"
"void main() {\n"
" out_Color.rgba = texture(tex, Texcoord);\n"
"}\n";
const char *image_gamma_frag_src =
"#version 400 core\n"
"uniform sampler2D tex;\n"
"uniform uint palette[256];\n"
"float gamma_ch(int ch, float col)\n"
"{\n"
" return float(bitfieldExtract(palette[uint(col * 255.0)], ch*8, 8)) / 255.0;\n"
"}\n"
"\n"
"vec4 gamma(vec4 col)\n"
"{\n"
" return vec4(gamma_ch(0, col.r), gamma_ch(1, col.g), gamma_ch(2, col.b), col.a);\n"
"}\n"
"in vec2 Texcoord;\n"
"out vec4 out_Color;\n"
"void main() {\n"
" out_Color.rgba = gamma(texture(tex, Texcoord));\n"
"}\n";
// Simple 2-color decal shader
// - in_ColorFill is first pass
// - Red channel of the texture is used as primary color, mixed with 1-Red for
// secondary color.
// - Blue is a lazy alpha removal for now
// - Alpha channel passed through
const char *mask_frag_src =
"#version 150 core\n"
"uniform sampler2D tex;\n"
"uniform vec4 in_ColorPrimary;\n"
"uniform vec4 in_ColorSecondary;\n"
"uniform vec4 in_ColorFill;\n"
"in vec2 Texcoord;\n"
"out vec4 out_Color;\n"
"void main() {\n"
" vec4 t = texture(tex, Texcoord);\n"
" out_Color.rgba = in_ColorFill.rgba;\n"
" out_Color.rgb += mix(in_ColorSecondary.rgb, in_ColorPrimary.rgb, t.r);\n"
" out_Color.a += t.a - t.b;\n"
"}\n";
const char *frag_src = NULL;
if (type == SHADER_TYPE_MASK) {
frag_src = mask_frag_src;
} else if (type == SHADER_TYPE_BLIT) {
frag_src = image_frag_src;
} else if (type == SHADER_TYPE_BLIT_GAMMA) {
frag_src = image_gamma_frag_src;
} else if (type == SHADER_TYPE_LOGO) {
frag_src = xemu_logo_frag_src;
} else {
assert(0);
}
GLuint frag = compile_shader(GL_FRAGMENT_SHADER, frag_src);
assert(frag != 0);
// Link vertex and fragment shaders
s->prog = glCreateProgram();
glAttachShader(s->prog, vert);
glAttachShader(s->prog, frag);
glBindFragDataLocation(s->prog, 0, "out_Color");
glLinkProgram(s->prog);
glUseProgram(s->prog);
// Flag shaders for deletion when program is deleted
glDeleteShader(vert);
glDeleteShader(frag);
s->FlipY_loc = glGetUniformLocation(s->prog, "in_FlipY");
s->ScaleOffset_loc = glGetUniformLocation(s->prog, "in_ScaleOffset");
s->TexScaleOffset_loc = glGetUniformLocation(s->prog, "in_TexScaleOffset");
s->tex_loc = glGetUniformLocation(s->prog, "tex");
s->ColorPrimary_loc = glGetUniformLocation(s->prog, "in_ColorPrimary");
s->ColorSecondary_loc = glGetUniformLocation(s->prog, "in_ColorSecondary");
s->ColorFill_loc = glGetUniformLocation(s->prog, "in_ColorFill");
s->time_loc = glGetUniformLocation(s->prog, "iTime");
s->scale_loc = glGetUniformLocation(s->prog, "scale");
for (int i = 0; i < 256; i++) {
char name[64];
snprintf(name, sizeof(name), "palette[%d]", i);
s->palette_loc[i] = glGetUniformLocation(s->prog, name);
}
// Create a vertex array object
glGenVertexArrays(1, &s->vao);
glBindVertexArray(s->vao);
// Populate vertex buffer
glGenBuffers(1, &s->vbo);
glBindBuffer(GL_ARRAY_BUFFER, s->vbo);
const GLfloat verts[6][4] = {
// x y s t
{ -1.0f, -1.0f, 0.0f, 0.0f }, // BL
{ -1.0f, 1.0f, 0.0f, 1.0f }, // TL
{ 1.0f, 1.0f, 1.0f, 1.0f }, // TR
{ 1.0f, -1.0f, 1.0f, 0.0f }, // BR
};
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_COPY);
// Populate element buffer
glGenBuffers(1, &s->ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->ebo);
const GLint indicies[] = { 0, 1, 2, 3 };
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicies), indicies, GL_STATIC_DRAW);
// Bind vertex position attribute
GLint pos_attr_loc = glGetAttribLocation(s->prog, "in_Position");
glVertexAttribPointer(pos_attr_loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(pos_attr_loc);
// Bind vertex texture coordinate attribute
GLint tex_attr_loc = glGetAttribLocation(s->prog, "in_Texcoord");
if (tex_attr_loc >= 0) {
glVertexAttribPointer(tex_attr_loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)(2*sizeof(GLfloat)));
glEnableVertexAttribArray(tex_attr_loc);
}
return s;
}
static GLuint load_texture(unsigned char *data, int width, int height, int channels)
{
GLuint tex;
glGenTextures(1, &tex);
assert(tex != 0);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
return tex;
}
GLuint load_texture_from_file(const char *name)
{
// Flip vertically so textures are loaded according to GL convention.
stbi_set_flip_vertically_on_load(1);
// Read file into memory
int width, height, channels = 0;
unsigned char *data = stbi_load(name, &width, &height, &channels, 4);
assert(data != NULL);
GLuint tex = load_texture(data, width, height, channels);
stbi_image_free(data);
return tex;
}
GLuint load_texture_from_memory(const unsigned char *buf, unsigned int size)
{
// Flip vertically so textures are loaded according to GL convention.
stbi_set_flip_vertically_on_load(1);
int width, height, channels = 0;
unsigned char *data = stbi_load_from_memory(buf, size, &width, &height, &channels, 4);
assert(data != NULL);
GLuint tex = load_texture(data, width, height, channels);
stbi_image_free(data);
return tex;
}
void render_decal(
struct decal_shader *s,
float x, float y, float w, float h,
float tex_x, float tex_y, float tex_w, float tex_h,
uint32_t primary, uint32_t secondary, uint32_t fill
)
{
GLint vp[4];
glGetIntegerv(GL_VIEWPORT, vp);
float ww = vp[2], wh = vp[3];
x = (int)x;
y = (int)y;
w = (int)w;
h = (int)h;
tex_x = (int)tex_x;
tex_y = (int)tex_y;
tex_w = (int)tex_w;
tex_h = (int)tex_h;
int tw_i, th_i;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw_i);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th_i);
float tw = tw_i, th = th_i;
#define COL(color, c) (float)(((color)>>((c)*8)) & 0xff)/255.0
glUniform1i(s->FlipY_loc, s->flip);
glUniform4f(s->ScaleOffset_loc, w/ww, h/wh, -1+((2*x+w)/ww), -1+((2*y+h)/wh));
glUniform4f(s->TexScaleOffset_loc, tex_w/tw, tex_h/th, tex_x/tw, tex_y/th);
glUniform1i(s->tex_loc, 0);
glUniform4f(s->ColorPrimary_loc, COL(primary, 3), COL(primary, 2), COL(primary, 1), COL(primary, 0));
glUniform4f(s->ColorSecondary_loc, COL(secondary, 3), COL(secondary, 2), COL(secondary, 1), COL(secondary, 0));
glUniform4f(s->ColorFill_loc, COL(fill, 3), COL(fill, 2), COL(fill, 1), COL(fill, 0));
if (s->time_loc >= 0) glUniform1f(s->time_loc, s->time/1000.0f);
if (s->scale_loc >= 0) glUniform1f(s->scale_loc, s->scale);
#undef COL
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL);
}
void render_decal_image(
struct decal_shader *s,
float x, float y, float w, float h,
float tex_x, float tex_y, float tex_w, float tex_h
)
{
GLint vp[4];
glGetIntegerv(GL_VIEWPORT, vp);
float ww = vp[2], wh = vp[3];
int tw_i, th_i;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw_i);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th_i);
float tw = tw_i, th = th_i;
glUniform1i(s->FlipY_loc, s->flip);
glUniform4f(s->ScaleOffset_loc, w/ww, h/wh, -1+((2*x+w)/ww), -1+((2*y+h)/wh));
glUniform4f(s->TexScaleOffset_loc, tex_w/tw, tex_h/th, tex_x/tw, tex_y/th);
glUniform1i(s->tex_loc, 0);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL);
}
struct fbo *create_fbo(int width, int height)
{
struct fbo *fbo = (struct fbo *)malloc(sizeof(struct fbo));
assert(fbo != NULL);
fbo->w = width;
fbo->h = height;
// Allocate the texture
glGenTextures(1, &fbo->tex);
glBindTexture(GL_TEXTURE_2D, fbo->tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fbo->w, fbo->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// Allocate the framebuffer object
glGenFramebuffers(1, &fbo->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbo->tex, 0);
GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, DrawBuffers);
return fbo;
}
static GLboolean m_blend;
void render_to_default_fb(void)
{
if (!m_blend) {
glDisable(GL_BLEND);
}
// Restore default framebuffer, viewport, blending funciton
glBindFramebuffer(GL_FRAMEBUFFER, main_fb);
glViewport(vp[0], vp[1], vp[2], vp[3]);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
GLuint render_to_fbo(struct fbo *fbo)
{
m_blend = glIsEnabled(GL_BLEND);
if (!m_blend) {
glEnable(GL_BLEND);
}
glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo);
glViewport(0, 0, fbo->w, fbo->h);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
return fbo->tex;
}

View file

@ -1,103 +0,0 @@
/*
* xemu User Interface Rendering Helpers
*
* Copyright (C) 2020-2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef XEMU_SHADERS_H
#define XEMU_SHADERS_H
#include <SDL2/SDL.h>
#include <epoxy/gl.h>
#include "stb_image.h"
enum SHADER_TYPE {
SHADER_TYPE_BLIT,
SHADER_TYPE_BLIT_GAMMA,
SHADER_TYPE_MASK,
SHADER_TYPE_LOGO,
};
struct decal_shader
{
int flip;
float scale;
float smoothing;
float outline_dist;
uint32_t time;
// GL object handles
GLuint prog, vao, vbo, ebo;
// Uniform locations
GLint Mat_loc;
GLint FlipY_loc;
GLint tex_loc;
GLint ScaleOffset_loc;
GLint TexScaleOffset_loc;
GLint ColorPrimary_loc;
GLint ColorSecondary_loc;
GLint ColorFill_loc;
GLint time_loc;
GLint scale_loc;
GLint palette_loc[256];
};
struct fbo {
GLuint fbo;
GLuint tex;
int w, h;
};
#ifdef __cplusplus
extern "C" {
#endif
extern GLuint main_fb;
extern GLint vp[4];
GLuint compile_shader(GLenum type, const char *src);
struct decal_shader *create_decal_shader(enum SHADER_TYPE type);
void delete_decal_shader(struct decal_shader *s);
GLuint load_texture_from_file(const char *name);
GLuint load_texture_from_memory(const unsigned char *buf, unsigned int size);
struct fbo *create_fbo(int width, int height);
void render_to_default_fb(void);
GLuint render_to_fbo(struct fbo *fbo);
void render_decal(
struct decal_shader *s,
float x, float y, float w, float h,
float tex_x, float tex_y, float tex_w, float tex_h,
uint32_t primary, uint32_t secondary, uint32_t fill
);
void render_decal_image(
struct decal_shader *s,
float x, float y, float w, float h,
float tex_x, float tex_y, float tex_w, float tex_h
);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -1,195 +0,0 @@
/*
* xemu Automatic Update
*
* Copyright (C) 2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <SDL_filesystem.h>
#include "util/miniz/miniz.h"
#include "xemu-update.h"
#include "xemu-version.h"
#if defined(_WIN32)
const char *version_host = "raw.githubusercontent.com";
const char *version_uri = "/mborgerson/xemu/ppa-snapshot/XEMU_VERSION";
const char *download_host = "github.com";
const char *download_uri = "/mborgerson/xemu/releases/latest/download/xemu-win-release.zip";
#else
FIXME
#endif
#define CPPHTTPLIB_OPENSSL_SUPPORT 1
#include "httplib.h"
#define DPRINTF(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__);
Updater::Updater()
{
m_status = UPDATER_IDLE;
m_update_availability = UPDATE_AVAILABILITY_UNKNOWN;
m_update_percentage = 0;
m_latest_version = "Unknown";
m_should_cancel = false;
}
void Updater::check_for_update(UpdaterCallback on_complete)
{
if (m_status == UPDATER_IDLE || m_status == UPDATER_ERROR) {
m_on_complete = on_complete;
qemu_thread_create(&m_thread, "update_worker",
&Updater::checker_thread_worker_func,
this, QEMU_THREAD_JOINABLE);
}
}
void *Updater::checker_thread_worker_func(void *updater)
{
((Updater *)updater)->check_for_update_internal();
return NULL;
}
void Updater::check_for_update_internal()
{
httplib::SSLClient cli(version_host, 443);
cli.set_follow_location(true);
cli.set_timeout_sec(5);
auto res = cli.Get(version_uri, [this](uint64_t len, uint64_t total) {
m_update_percentage = len*100/total;
return !m_should_cancel;
});
if (m_should_cancel) {
m_should_cancel = false;
m_status = UPDATER_IDLE;
goto finished;
} else if (!res || res->status != 200) {
m_status = UPDATER_ERROR;
goto finished;
}
if (strcmp(xemu_version, res->body.c_str())) {
m_update_availability = UPDATE_AVAILABLE;
} else {
m_update_availability = UPDATE_NOT_AVAILABLE;
}
m_latest_version = res->body;
m_status = UPDATER_IDLE;
finished:
if (m_on_complete) {
m_on_complete();
}
}
void Updater::update()
{
if (m_status == UPDATER_IDLE || m_status == UPDATER_ERROR) {
m_status = UPDATER_UPDATING;
qemu_thread_create(&m_thread, "update_worker",
&Updater::update_thread_worker_func,
this, QEMU_THREAD_JOINABLE);
}
}
void *Updater::update_thread_worker_func(void *updater)
{
((Updater *)updater)->update_internal();
return NULL;
}
void Updater::update_internal()
{
httplib::SSLClient cli(download_host, 443);
cli.set_follow_location(true);
cli.set_timeout_sec(5);
auto res = cli.Get(download_uri, [this](uint64_t len, uint64_t total) {
m_update_percentage = len*100/total;
return !m_should_cancel;
});
if (m_should_cancel) {
m_should_cancel = false;
m_status = UPDATER_IDLE;
return;
} else if (!res || res->status != 200) {
m_status = UPDATER_ERROR;
return;
}
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
if (!mz_zip_reader_init_mem(&zip, res->body.data(), res->body.size(), 0)) {
DPRINTF("mz_zip_reader_init_mem failed\n");
m_status = UPDATER_ERROR;
return;
}
mz_uint num_files = mz_zip_reader_get_num_files(&zip);
for (mz_uint file_idx = 0; file_idx < num_files; file_idx++) {
mz_zip_archive_file_stat fstat;
if (!mz_zip_reader_file_stat(&zip, file_idx, &fstat)) {
DPRINTF("mz_zip_reader_file_stat failed for file #%d\n", file_idx);
goto errored;
}
if (fstat.m_filename[strlen(fstat.m_filename)-1] == '/') {
/* FIXME: mkdirs */
DPRINTF("FIXME: subdirs not handled yet\n");
goto errored;
}
char *dst_path = g_strdup_printf("%s%s", SDL_GetBasePath(), fstat.m_filename);
DPRINTF("extracting %s to %s\n", fstat.m_filename, dst_path);
if (!strcmp(fstat.m_filename, "xemu.exe")) {
// We cannot overwrite current executable, but we can move it
char *renamed_path = g_strdup_printf("%s%s", SDL_GetBasePath(), "xemu-previous.exe");
MoveFileExA(dst_path, renamed_path, MOVEFILE_REPLACE_EXISTING);
g_free(renamed_path);
}
if (!mz_zip_reader_extract_to_file(&zip, file_idx, dst_path, 0)) {
DPRINTF("mz_zip_reader_extract_to_file failed to create %s\n", dst_path);
g_free(dst_path);
goto errored;
}
g_free(dst_path);
}
m_status = UPDATER_UPDATE_SUCCESSFUL;
goto cleanup_zip;
errored:
m_status = UPDATER_ERROR;
cleanup_zip:
mz_zip_reader_end(&zip);
}
extern "C" {
extern char **gArgv;
}
void Updater::restart_to_updated()
{
char *target_exec = g_strdup_printf("%s%s", SDL_GetBasePath(), "xemu.exe");
DPRINTF("Restarting to updated executable %s\n", target_exec);
_execv(target_exec, gArgv);
DPRINTF("Launching updated executable failed\n");
exit(1);
}

View file

@ -1,80 +0,0 @@
/*
* xemu Automatic Update
*
* Copyright (C) 2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef XEMU_UPDATE_H
#define XEMU_UPDATE_H
#include <string>
#include <stdint.h>
#include <functional>
extern "C" {
#include "qemu/osdep.h"
#include "qemu-common.h"
#include "qemu/thread.h"
}
typedef enum {
UPDATE_AVAILABILITY_UNKNOWN,
UPDATE_NOT_AVAILABLE,
UPDATE_AVAILABLE
} UpdateAvailability;
typedef enum {
UPDATER_IDLE,
UPDATER_ERROR,
UPDATER_CHECKING_FOR_UPDATE,
UPDATER_UPDATING,
UPDATER_UPDATE_SUCCESSFUL
} UpdateStatus;
using UpdaterCallback = std::function<void(void)>;
class Updater {
private:
UpdateAvailability m_update_availability;
int m_update_percentage;
QemuThread m_thread;
std::string m_latest_version;
bool m_should_cancel;
UpdateStatus m_status;
UpdaterCallback m_on_complete;
public:
Updater();
UpdateStatus get_status() { return m_status; }
UpdateAvailability get_update_availability() { return m_update_availability; }
bool is_errored() { return m_status == UPDATER_ERROR; }
bool is_pending_restart() { return m_status == UPDATER_UPDATE_SUCCESSFUL; }
bool is_update_available() { return m_update_availability == UPDATE_AVAILABLE; }
bool is_checking_for_update() { return m_status == UPDATER_CHECKING_FOR_UPDATE; }
bool is_updating() { return m_status == UPDATER_UPDATING; }
std::string get_update_version() { return m_latest_version; }
void cancel() { m_should_cancel = true; }
void update();
void update_internal();
void check_for_update(UpdaterCallback on_complete = nullptr);
void check_for_update_internal();
int get_update_progress_percentage() { return m_update_percentage; }
static void *update_thread_worker_func(void *updater);
static void *checker_thread_worker_func(void *updater);
void restart_to_updated(void);
};
#endif

132
ui/xemu.c
View file

@ -43,10 +43,10 @@
#include "sysemu/runstate.h"
#include "sysemu/runstate-action.h"
#include "sysemu/sysemu.h"
#include "xemu-hud.h"
#include "xui/xemu-hud.h"
#include "xemu-input.h"
#include "xemu-settings.h"
#include "xemu-shaders.h"
// #include "xemu-shaders.h"
#include "xemu-version.h"
#include "xemu-os-utils.h"
@ -55,6 +55,8 @@
#include "hw/xbox/smbus.h" // For eject, drive tray
#include "hw/xbox/nv2a/nv2a.h"
#include <stb_image.h>
#ifdef _WIN32
// Provide hint to prefer high-performance graphics for hybrid systems
// https://gpuopen.com/learn/amdpowerxpressrequesthighperformance/
@ -108,7 +110,7 @@ static SDL_Cursor *guest_sprite;
static Notifier mouse_mode_notifier;
static SDL_Window *m_window;
static SDL_GLContext m_context;
struct decal_shader *blit;
// struct decal_shader *blit;
static QemuSemaphore display_init_sem;
@ -845,28 +847,58 @@ static void sdl2_display_very_early_init(DisplayOptions *o)
#endif
, xemu_version);
// Decide window size
int min_window_width = 640;
int min_window_height = 480;
int window_width = min_window_width;
int window_height = min_window_height;
const int res_table[][2] = {
{640, 480},
{1280, 720},
{1280, 800},
{1280, 960},
{1920, 1080},
{2560, 1440},
{2560, 1600},
{2560, 1920},
{3840, 2160}
};
if (g_config.display.window.startup_size == CONFIG_DISPLAY_WINDOW_STARTUP_SIZE_LAST_USED) {
window_width = g_config.display.window.last_width;
window_height = g_config.display.window.last_height;
} else {
window_width = res_table[g_config.display.window.startup_size-1][0];
window_height = res_table[g_config.display.window.startup_size-1][1];
}
if (window_width < min_window_width) {
window_width = min_window_width;
}
if (window_height < min_window_height) {
window_height = min_window_height;
}
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
// Create main window
m_window = SDL_CreateWindow(
title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, window_width, window_height,
window_flags);
if (m_window == NULL) {
fprintf(stderr, "Failed to create main window\n");
SDL_Quit();
exit(1);
}
g_free(title);
SDL_SetWindowMinimumSize(m_window, min_window_width, min_window_height);
SDL_DisplayMode disp_mode;
SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(m_window), &disp_mode);
int win_w = g_config.display.window.last_width,
win_h = g_config.display.window.last_height;
if (win_w > 0 && win_h > 0) {
if (disp_mode.w >= win_w && disp_mode.h >= win_h) {
SDL_SetWindowSize(m_window, win_w, win_h);
SDL_SetWindowPosition(m_window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
if (disp_mode.w < window_width || disp_mode.h < window_height) {
SDL_SetWindowSize(m_window, min_window_width, min_window_height);
SDL_SetWindowPosition(m_window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
m_context = SDL_GL_CreateContext(m_window);
@ -923,9 +955,9 @@ static void sdl2_display_early_init(DisplayOptions *o)
display_opengl = 1;
SDL_GL_MakeCurrent(m_window, m_context);
SDL_GL_SetSwapInterval(0);
SDL_GL_SetSwapInterval(g_config.display.window.vsync ? 1 : 0);
xemu_hud_init(m_window, m_context);
blit = create_decal_shader(SHADER_TYPE_BLIT_GAMMA);
// blit = create_decal_shader(SHADER_TYPE_BLIT_GAMMA);
}
static void sdl2_display_init(DisplayState *ds, DisplayOptions *o)
@ -942,6 +974,8 @@ static void sdl2_display_init(DisplayState *ds, DisplayOptions *o)
gui_fullscreen = o->has_full_screen && o->full_screen;
gui_fullscreen |= g_config.display.window.fullscreen_on_startup;
#if 1
// Explicitly set number of outputs to 1 for a single screen. We don't need
// multiple for now, but maybe in the future debug stuff can go on a second
@ -1145,6 +1179,7 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl)
*/
GLuint tex = nv2a_get_framebuffer_surface();
if (tex == 0) {
// FIXME: Don't upload if notdirty
xb_surface_gl_create_texture(scon->surface);
scon->updates++;
tex = scon->surface->texture;
@ -1160,72 +1195,9 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl)
qemu_mutex_lock_iothread();
sdl2_poll_events(scon);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex);
// Get texture dimensions
int tw, th;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th);
// Get window dimensions
int ww, wh;
SDL_GL_GetDrawableSize(scon->real_window, &ww, &wh);
// Calculate scaling factors
float scale[2];
if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) {
// Stretch to fit
scale[0] = 1.0;
scale[1] = 1.0;
} else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_CENTER) {
// Centered
scale[0] = (float)tw/(float)ww;
scale[1] = (float)th/(float)wh;
} else {
float t_ratio;
if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) {
// Scale to fit window using a fixed 16:9 aspect ratio
t_ratio = 16.0f/9.0f;
} else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) {
t_ratio = 4.0f/3.0f;
} else {
// Scale to fit, preserving framebuffer aspect ratio
t_ratio = (float)tw/(float)th;
}
float w_ratio = (float)ww/(float)wh;
if (w_ratio >= t_ratio) {
scale[0] = t_ratio/w_ratio;
scale[1] = 1.0;
} else {
scale[0] = 1.0;
scale[1] = w_ratio/t_ratio;
}
}
// Render framebuffer and GUI
struct decal_shader *s = blit;
s->flip = flip_required;
glViewport(0, 0, ww, wh);
glUseProgram(s->prog);
glBindVertexArray(s->vao);
glUniform1i(s->FlipY_loc, s->flip);
glUniform4f(s->ScaleOffset_loc, scale[0], scale[1], 0, 0);
glUniform4f(s->TexScaleOffset_loc, 1.0, 1.0, 0, 0);
glUniform1i(s->tex_loc, 0);
const uint8_t *palette = nv2a_get_dac_palette();
for (int i = 0; i < 256; i++) {
uint32_t e = (palette[i * 3 + 2] << 16) | (palette[i * 3 + 1] << 8) |
palette[i * 3];
glUniform1ui(s->palette_loc[i], e);
}
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL);
xemu_hud_set_framebuffer_texture(tex, flip_required);
xemu_hud_render();
// Release BQL before swapping (which may sleep if swap interval is not immediate)

65
ui/xui/actions.cc Normal file
View file

@ -0,0 +1,65 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "common.hh"
#include "misc.hh"
#include "xemu-hud.h"
void ActionEjectDisc(void)
{
xemu_settings_set_string(&g_config.sys.files.dvd_path, "");
xemu_eject_disc();
}
void ActionLoadDisc(void)
{
const char *iso_file_filters = ".iso Files\0*.iso\0All Files\0*.*\0";
const char *new_disc_path =
PausedFileOpen(NOC_FILE_DIALOG_OPEN, iso_file_filters,
g_config.sys.files.dvd_path, NULL);
if (new_disc_path == NULL) {
/* Cancelled */
return;
}
xemu_settings_set_string(&g_config.sys.files.dvd_path, new_disc_path);
xemu_load_disc(new_disc_path);
}
void ActionTogglePause(void)
{
if (runstate_is_running()) {
vm_stop(RUN_STATE_PAUSED);
} else {
vm_start();
}
}
void ActionReset(void)
{
qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
}
void ActionShutdown(void)
{
qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI);
}
void ActionScreenshot(void)
{
g_screenshot_pending = true;
}

26
ui/xui/actions.hh Normal file
View file

@ -0,0 +1,26 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
void ActionEjectDisc();
void ActionLoadDisc();
void ActionTogglePause();
void ActionReset();
void ActionShutdown();
void ActionScreenshot();

161
ui/xui/animation.cc Normal file
View file

@ -0,0 +1,161 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include <cmath>
#include "common.hh"
#include "animation.hh"
Animation::Animation(float duration)
: m_duration(duration)
{
Reset();
}
void Animation::Reset()
{
m_acc = 0;
}
void Animation::SetDuration(float duration)
{
m_duration = duration;
}
void Animation::Step()
{
if (g_config.display.ui.use_animations) {
ImGuiIO &io = ImGui::GetIO();
m_acc += io.DeltaTime;
} else {
m_acc = m_duration;
}
}
bool Animation::IsComplete()
{
return m_acc >= m_duration;
}
float Animation::GetLinearValue()
{
if (m_acc < m_duration) {
return m_acc / m_duration;
} else {
return 1.0;
}
}
void Animation::SetLinearValue(float t)
{
m_acc = t * m_duration;
}
float Animation::GetSinInterpolatedValue()
{
return sin(GetLinearValue() * M_PI * 0.5);
}
EasingAnimation::EasingAnimation(float ease_in_duration, float ease_out_duration)
: m_state(AnimationState::PreEasingIn),
m_duration_out(ease_out_duration),
m_duration_in(ease_in_duration) {}
void EasingAnimation::EaseIn()
{
EaseIn(m_duration_in);
}
void EasingAnimation::EaseIn(float duration)
{
if (duration == 0) {
m_state = AnimationState::Idle;
return;
}
float t = m_animation.GetLinearValue();
m_animation.SetDuration(duration);
if (m_state == AnimationState::EasingOut) {
m_animation.SetLinearValue(1-t);
} else if (m_state != AnimationState::EasingIn) {
m_animation.Reset();
}
m_state = AnimationState::EasingIn;
}
void EasingAnimation::EaseOut()
{
EaseOut(m_duration_out);
}
void EasingAnimation::EaseOut(float duration)
{
if (duration == 0) {
m_state = AnimationState::PostEasingOut;
return;
}
float t = m_animation.GetLinearValue();
m_animation.SetDuration(duration);
if (m_state == AnimationState::EasingIn) {
m_animation.SetLinearValue(1-t);
} else if (m_state != AnimationState::EasingOut) {
m_animation.Reset();
}
m_state = AnimationState::EasingOut;
}
void EasingAnimation::Step()
{
if (m_state == AnimationState::EasingIn ||
m_state == AnimationState::EasingOut) {
m_animation.Step();
if (m_animation.IsComplete()) {
if (m_state == AnimationState::EasingIn) {
m_state = AnimationState::Idle;
} else if (m_state == AnimationState::EasingOut) {
m_state = AnimationState::PostEasingOut;
}
}
}
}
float EasingAnimation::GetLinearValue()
{
switch (m_state) {
case AnimationState::PreEasingIn: return 0;
case AnimationState::EasingIn: return m_animation.GetLinearValue();
case AnimationState::Idle: return 1;
case AnimationState::EasingOut: return 1 - m_animation.GetLinearValue();
case AnimationState::PostEasingOut: return 0;
default: return 0;
}
}
float EasingAnimation::GetSinInterpolatedValue()
{
return sin(GetLinearValue() * M_PI * 0.5);
}
bool EasingAnimation::IsAnimating()
{
return m_state == AnimationState::EasingIn ||
m_state == AnimationState::EasingOut;
}
bool EasingAnimation::IsComplete()
{
return m_state == AnimationState::PostEasingOut;
}

73
ui/xui/animation.hh Normal file
View file

@ -0,0 +1,73 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "common.hh"
const ImVec2 EASE_VECTOR_DOWN = ImVec2(0, -25);
const ImVec2 EASE_VECTOR_LEFT = ImVec2(25, 0);
const ImVec2 EASE_VECTOR_RIGHT = ImVec2(-25, 0);
enum AnimationState
{
PreEasingIn,
EasingIn,
Idle,
EasingOut,
PostEasingOut
};
// Step a value from 0 to 1 over some duration of time.
class Animation
{
protected:
float m_duration;
float m_acc;
public:
Animation(float duration = 0);
void Reset();
void SetDuration(float duration);
void Step();
bool IsComplete();
float GetLinearValue();
void SetLinearValue(float t);
float GetSinInterpolatedValue();
};
// Stateful animation sequence for easing in and out: 0->1->0
class EasingAnimation
{
protected:
AnimationState m_state;
Animation m_animation;
float m_duration_out;
float m_duration_in;
public:
EasingAnimation(float ease_in_duration = 1.0, float ease_out_duration = 1.0);
void EaseIn();
void EaseIn(float duration);
void EaseOut();
void EaseOut(float duration);
void Step();
float GetLinearValue();
float GetSinInterpolatedValue();
bool IsAnimating();
bool IsComplete();
};

55
ui/xui/common.hh Normal file
View file

@ -0,0 +1,55 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <SDL.h>
#include <epoxy/gl.h>
#include "ui/xemu-settings.h"
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_impl_sdl.h>
#include <imgui_impl_opengl3.h>
#include <implot.h>
#include <stb_image.h>
extern "C" {
#include <noc_file_dialog.h>
// Include necessary QEMU headers
#include "qemu/osdep.h"
#include "qemu-common.h"
#include "qapi/error.h"
#include "sysemu/sysemu.h"
#include "sysemu/runstate.h"
#include "hw/xbox/mcpx/apu_debug.h"
#include "hw/xbox/nv2a/debug.h"
#include "hw/xbox/nv2a/nv2a.h"
#undef typename
#undef atomic_fetch_add
#undef atomic_fetch_and
#undef atomic_fetch_xor
#undef atomic_fetch_or
#undef atomic_fetch_sub
}
extern bool g_screenshot_pending;
extern float g_main_menu_height;

220
ui/xui/compat.cc Normal file
View file

@ -0,0 +1,220 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include <string>
#include "common.hh"
#include "compat.hh"
#include "widgets.hh"
#include "viewport-manager.hh"
#include "font-manager.hh"
#include "xemu-version.h"
#include "reporting.hh"
#include "../xemu-settings.h"
#include "../xemu-os-utils.h"
CompatibilityReporter::CompatibilityReporter()
{
is_open = false;
report.token = "";
report.xemu_version = xemu_version;
report.xemu_branch = xemu_branch;
report.xemu_commit = xemu_commit;
report.xemu_date = xemu_date;
report.os_platform = xemu_get_os_platform();
report.os_version = xemu_get_os_info();
report.cpu = xemu_get_cpu_info();
dirty = true;
is_xbe_identified = false;
did_send = send_result = false;
}
CompatibilityReporter::~CompatibilityReporter()
{
}
void CompatibilityReporter::Draw()
{
if (!is_open) return;
const char *playability_names[] = {
"Broken",
"Intro",
"Starts",
"Playable",
"Perfect",
};
const char *playability_descriptions[] = {
"This title crashes very soon after launching, or displays nothing at all.",
"This title displays an intro sequence, but fails to make it to gameplay.",
"This title starts, but may crash or have significant issues.",
"This title is playable, but may have minor issues.",
"This title is playable from start to finish with no noticable issues."
};
ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_viewport_mgr.m_scale, 0.0f));
if (!ImGui::Begin("Report Compatibility", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
if (ImGui::IsWindowAppearing()) {
report.gl_vendor = (const char *)glGetString(GL_VENDOR);
report.gl_renderer = (const char *)glGetString(GL_RENDERER);
report.gl_version = (const char *)glGetString(GL_VERSION);
report.gl_shading_language_version = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
struct xbe *xbe = xemu_get_xbe_info();
is_xbe_identified = xbe != NULL;
if (is_xbe_identified) {
report.SetXbeData(xbe);
}
did_send = send_result = false;
playability = 3; // Playable
report.compat_rating = playability_names[playability];
description[0] = '\x00';
report.compat_comments = description;
strncpy(token_buf, g_config.general.user_token, sizeof(token_buf)-1);
report.token = token_buf;
dirty = true;
}
if (!is_xbe_identified) {
ImGui::TextWrapped(
"An XBE could not be identified. Please launch an official "
"Xbox title to submit a compatibility report.");
ImGui::End();
return;
}
ImGui::TextWrapped(
"If you would like to help improve xemu by submitting a compatibility report for this "
"title, please select an appropriate playability level, enter a "
"brief description, then click 'Send'."
"\n\n"
"Note: By submitting a report, you acknowledge and consent to "
"collection, archival, and publication of information as outlined "
"in 'Privacy Disclosure' below.");
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
ImGui::Separator();
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
ImGui::Columns(2, "", false);
ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.25);
ImGui::Text("User Token");
ImGui::SameLine();
HelpMarker("This is a unique access token used to authorize submission of the report. To request a token, click 'Get Token'.");
ImGui::NextColumn();
float item_width = ImGui::GetColumnWidth()*0.75-20*g_viewport_mgr.m_scale;
ImGui::SetNextItemWidth(item_width);
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
if (ImGui::InputText("###UserToken", token_buf, sizeof(token_buf), 0)) {
xemu_settings_set_string(&g_config.general.user_token, token_buf);
report.token = token_buf;
dirty = true;
}
ImGui::PopFont();
ImGui::SameLine();
if (ImGui::Button("Get Token")) {
xemu_open_web_browser("https://reports.xemu.app");
}
ImGui::NextColumn();
ImGui::Text("Playability");
ImGui::NextColumn();
ImGui::SetNextItemWidth(item_width);
if (ImGui::Combo("###PlayabilityRating", &playability,
"Broken\0" "Intro/Menus\0" "Starts\0" "Playable\0" "Perfect\0")) {
report.compat_rating = playability_names[playability];
dirty = true;
}
ImGui::SameLine();
HelpMarker(playability_descriptions[playability]);
ImGui::NextColumn();
ImGui::Columns(1);
ImGui::Text("Description");
if (ImGui::InputTextMultiline("###desc", description, sizeof(description), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 6), 0)) {
report.compat_comments = description;
dirty = true;
}
if (ImGui::TreeNode("Report Details")) {
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
if (dirty) {
serialized_report = report.GetSerializedReport();
dirty = false;
}
ImGui::InputTextMultiline("##build_info", (char*)serialized_report.c_str(), strlen(serialized_report.c_str())+1, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 7), ImGuiInputTextFlags_ReadOnly);
ImGui::PopFont();
ImGui::TreePop();
}
if (ImGui::TreeNode("Privacy Disclosure (Please read before submission!)")) {
ImGui::TextWrapped(
"By volunteering to submit a compatibility report, basic information about your "
"computer is collected, including: your operating system version, CPU model, "
"graphics card/driver information, and details about the title which are "
"extracted from the executable in memory. The contents of this report can be "
"seen before submission by expanding 'Report Details'."
"\n\n"
"Like many websites, upon submission, the public IP address of your computer is "
"also recorded with your report. If provided, the identity associated with your "
"token is also recorded."
"\n\n"
"This information will be archived and used to analyze, resolve problems with, "
"and improve the application. This information may be made publicly visible, "
"for example: to anyone who wishes to see the playability status of a title, as "
"indicated by your report.");
ImGui::TreePop();
}
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
ImGui::Separator();
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
if (did_send) {
if (send_result) {
ImGui::Text("Sent! Thanks.");
} else {
ImGui::Text("Error: %s (%d)", report.GetResultMessage().c_str(), report.GetResultCode());
}
ImGui::SameLine();
}
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)*g_viewport_mgr.m_scale);
ImGui::SetItemDefaultFocus();
if (ImGui::Button("Send", ImVec2(120*g_viewport_mgr.m_scale, 0))) {
did_send = true;
send_result = report.Send();
if (send_result) {
is_open = false;
}
}
ImGui::End();
}
CompatibilityReporter compatibility_reporter_window;

41
ui/xui/compat.hh Normal file
View file

@ -0,0 +1,41 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <string>
#include "reporting.hh"
class CompatibilityReporter
{
public:
CompatibilityReport report;
bool dirty;
bool is_open;
bool is_xbe_identified;
bool did_send, send_result;
char token_buf[512];
int playability;
char description[1024];
std::string serialized_report;
CompatibilityReporter();
~CompatibilityReporter();
void Draw();
};
extern CompatibilityReporter compatibility_reporter_window;

323
ui/xui/debug.cc Normal file
View file

@ -0,0 +1,323 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "debug.hh"
#include "common.hh"
#include "misc.hh"
#include "font-manager.hh"
#include "viewport-manager.hh"
DebugApuWindow::DebugApuWindow() : m_is_open(false)
{
}
void DebugApuWindow::Draw()
{
if (!m_is_open)
return;
ImGui::SetNextWindowContentSize(ImVec2(600.0f*g_viewport_mgr.m_scale, 0.0f));
if (!ImGui::Begin("Audio Debug", &m_is_open,
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
const struct McpxApuDebug *dbg = mcpx_apu_get_debug_info();
ImGui::Columns(2, "", false);
int now = SDL_GetTicks() % 1000;
float t = now/1000.0f;
float freq = 1;
float v = fabs(sin(M_PI*t*freq));
float c_active = mix(0.4, 0.97, v);
float c_inactive = 0.2f;
int voice_monitor = -1;
int voice_info = -1;
int voice_mute = -1;
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
for (int i = 0; i < 256; i++)
{
if (i % 16) {
ImGui::SameLine();
}
float c, s, h;
h = 0.6;
if (dbg->vp.v[i].active) {
if (dbg->vp.v[i].paused) {
c = c_inactive;
s = 0.4;
} else {
c = c_active;
s = 0.7;
}
if (mcpx_apu_debug_is_muted(i)) {
h = 1.0;
}
} else {
c = c_inactive;
s = 0;
}
ImGui::PushID(i);
ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(h, s, c));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(h, s, 0.8));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(h, 0.8f, 1.0));
char buf[12];
snprintf(buf, sizeof(buf), "%02x", i);
ImGui::Button(buf);
if (/*dbg->vp.v[i].active &&*/ ImGui::IsItemHovered()) {
voice_monitor = i;
voice_info = i;
}
if (ImGui::IsItemClicked(1)) {
voice_mute = i;
}
ImGui::PopStyleColor(3);
ImGui::PopID();
}
ImGui::PopStyleVar(3);
ImGui::PopFont();
if (voice_info >= 0) {
const struct McpxApuDebugVoice *voice = &dbg->vp.v[voice_info];
ImGui::BeginTooltip();
bool is_paused = voice->paused;
ImGui::Text("Voice 0x%x/%d %s", voice_info, voice_info, is_paused ? "(Paused)" : "");
ImGui::SameLine();
ImGui::Text(voice->stereo ? "Stereo" : "Mono");
ImGui::Separator();
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
const char *noyes[2] = { "NO", "YES" };
ImGui::Text("Stream: %-3s Loop: %-3s Persist: %-3s Multipass: %-3s "
"Linked: %-3s",
noyes[voice->stream], noyes[voice->loop],
noyes[voice->persist], noyes[voice->multipass],
noyes[voice->linked]);
const char *cs[4] = { "1 byte", "2 bytes", "ADPCM", "4 bytes" };
const char *ss[4] = {
"Unsigned 8b PCM",
"Signed 16b PCM",
"Signed 24b PCM",
"Signed 32b PCM"
};
assert(voice->container_size < 4);
assert(voice->sample_size < 4);
ImGui::Text("Container Size: %s, Sample Size: %s, Samples per Block: %d",
cs[voice->container_size], ss[voice->sample_size], voice->samples_per_block);
ImGui::Text("Rate: %f (%d Hz)", voice->rate, (int)(48000.0/voice->rate));
ImGui::Text("EBO=%d CBO=%d LBO=%d BA=%x",
voice->ebo, voice->cbo, voice->lbo, voice->ba);
ImGui::Text("Mix: ");
for (int i = 0; i < 8; i++) {
if (i == 4) ImGui::Text(" ");
ImGui::SameLine();
char buf[64];
if (voice->vol[i] == 0xFFF) {
snprintf(buf, sizeof(buf),
"Bin %2d (MUTE) ", voice->bin[i]);
} else {
snprintf(buf, sizeof(buf),
"Bin %2d (-%.3f) ", voice->bin[i],
(float)((voice->vol[i] >> 6) & 0x3f) +
(float)((voice->vol[i] >> 0) & 0x3f) / 64.0);
}
ImGui::Text("%-17s", buf);
}
ImGui::PopFont();
ImGui::EndTooltip();
}
if (voice_monitor >= 0) {
mcpx_apu_debug_isolate_voice(voice_monitor);
} else {
mcpx_apu_debug_clear_isolations();
}
if (voice_mute >= 0) {
mcpx_apu_debug_toggle_mute(voice_mute);
}
ImGui::SameLine();
ImGui::SetColumnWidth(0, ImGui::GetCursorPosX());
ImGui::NextColumn();
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
ImGui::Text("Frames: %04d", dbg->frames_processed);
ImGui::Text("GP Cycles: %04d", dbg->gp.cycles);
ImGui::Text("EP Cycles: %04d", dbg->ep.cycles);
bool color = (dbg->utilization > 0.9);
if (color) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0,0,1));
ImGui::Text("Utilization: %.2f%%", (dbg->utilization*100));
if (color) ImGui::PopStyleColor();
ImGui::PopFont();
static int mon = 0;
mon = mcpx_apu_debug_get_monitor();
if (ImGui::Combo("Monitor", &mon, "AC97\0VP Only\0GP Only\0EP Only\0GP/EP if enabled\0")) {
mcpx_apu_debug_set_monitor(mon);
}
static bool gp_realtime;
gp_realtime = dbg->gp_realtime;
if (ImGui::Checkbox("GP Realtime\n", &gp_realtime)) {
mcpx_apu_debug_set_gp_realtime_enabled(gp_realtime);
}
static bool ep_realtime;
ep_realtime = dbg->ep_realtime;
if (ImGui::Checkbox("EP Realtime\n", &ep_realtime)) {
mcpx_apu_debug_set_ep_realtime_enabled(ep_realtime);
}
ImGui::Columns(1);
ImGui::End();
}
// Utility structure for realtime plot
struct ScrollingBuffer {
int MaxSize;
int Offset;
ImVector<ImVec2> Data;
ScrollingBuffer() {
MaxSize = 2000;
Offset = 0;
Data.reserve(MaxSize);
}
void AddPoint(float x, float y) {
if (Data.size() < MaxSize)
Data.push_back(ImVec2(x,y));
else {
Data[Offset] = ImVec2(x,y);
Offset = (Offset + 1) % MaxSize;
}
}
void Erase() {
if (Data.size() > 0) {
Data.shrink(0);
Offset = 0;
}
}
};
DebugVideoWindow::DebugVideoWindow()
{
m_is_open = false;
m_transparent = false;
}
void DebugVideoWindow::Draw()
{
if (!m_is_open)
return;
float alpha = m_transparent ? 0.2 : 1.0;
PushWindowTransparencySettings(m_transparent, 0.2);
ImGui::SetNextWindowSize(ImVec2(600.0f*g_viewport_mgr.m_scale, 150.0f*g_viewport_mgr.m_scale), ImGuiCond_Once);
if (ImGui::Begin("Video Debug", &m_is_open)) {
double x_start, x_end;
static ImPlotAxisFlags rt_axis = ImPlotAxisFlags_NoTickLabels;
ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(5,5));
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f);
static ScrollingBuffer fps;
static float t = 0;
if (runstate_is_running()) {
t += ImGui::GetIO().DeltaTime;
fps.AddPoint(t, g_nv2a_stats.increment_fps);
}
x_start = t - 10.0;
x_end = t;
float plot_width = 0.5 * (ImGui::GetWindowSize().x -
2 * ImGui::GetStyle().WindowPadding.x -
ImGui::GetStyle().ItemSpacing.x);
ImGui::SetNextWindowBgAlpha(alpha);
if (ImPlot::BeginPlot("##ScrollingFPS", ImVec2(plot_width,75*g_viewport_mgr.m_scale))) {
ImPlot::SetupAxes(NULL, NULL, rt_axis, rt_axis | ImPlotAxisFlags_Lock);
ImPlot::SetupAxesLimits(x_start, x_end, 0, 65, ImPlotCond_Always);
if (fps.Data.size() > 0) {
ImPlot::PlotShaded("##fps", &fps.Data[0].x, &fps.Data[0].y, fps.Data.size(), 0, fps.Offset, 2 * sizeof(float));
ImPlot::PlotLine("##fps", &fps.Data[0].x, &fps.Data[0].y, fps.Data.size(), fps.Offset, 2 * sizeof(float));
}
ImPlot::Annotation(x_start, 65, ImPlot::GetLastItemColor(), ImVec2(0,0), true, "FPS: %d", g_nv2a_stats.increment_fps);
ImPlot::EndPlot();
}
ImGui::SameLine();
x_end = g_nv2a_stats.frame_count;
x_start = x_end - NV2A_PROF_NUM_FRAMES;
ImPlot::PushStyleColor(ImPlotCol_Line, ImPlot::GetColormapColor(1));
ImGui::SetNextWindowBgAlpha(alpha);
if (ImPlot::BeginPlot("##ScrollingMSPF", ImVec2(plot_width,75*g_viewport_mgr.m_scale))) {
ImPlot::SetupAxes(NULL, NULL, rt_axis, rt_axis | ImPlotAxisFlags_Lock);
ImPlot::SetupAxesLimits(x_start, x_end, 0, 100, ImPlotCond_Always);
ImPlot::PlotShaded("##mspf", &g_nv2a_stats.frame_history[0].mspf, NV2A_PROF_NUM_FRAMES, 0, 1, x_start, g_nv2a_stats.frame_ptr, sizeof(g_nv2a_stats.frame_working));
ImPlot::PlotLine("##mspf", &g_nv2a_stats.frame_history[0].mspf, NV2A_PROF_NUM_FRAMES, 1, x_start, g_nv2a_stats.frame_ptr, sizeof(g_nv2a_stats.frame_working));
ImPlot::Annotation(x_start, 100, ImPlot::GetLastItemColor(), ImVec2(0,0), true, "MSPF: %d", g_nv2a_stats.frame_history[(g_nv2a_stats.frame_ptr - 1) % NV2A_PROF_NUM_FRAMES].mspf);
ImPlot::EndPlot();
}
ImPlot::PopStyleColor();
if (ImGui::TreeNode("Advanced")) {
ImGui::SetNextWindowBgAlpha(alpha);
if (ImPlot::BeginPlot("##ScrollingDraws", ImVec2(-1,-1))) {
ImPlot::SetupAxes(NULL, NULL, rt_axis, rt_axis | ImPlotAxisFlags_Lock);
ImPlot::SetupAxesLimits(x_start, x_end, 0, 1500, ImPlotCond_Always);
for (int i = 0; i < NV2A_PROF__COUNT; i++) {
ImGui::PushID(i);
char title[64];
snprintf(title, sizeof(title), "%s: %d",
nv2a_profile_get_counter_name(i),
nv2a_profile_get_counter_value(i));
ImPlot::PushStyleColor(ImPlotCol_Line, ImPlot::GetColormapColor(i));
ImPlot::PushStyleColor(ImPlotCol_Fill, ImPlot::GetColormapColor(i));
ImPlot::PlotLine(title, &g_nv2a_stats.frame_history[0].counters[i], NV2A_PROF_NUM_FRAMES, 1, x_start, g_nv2a_stats.frame_ptr, sizeof(g_nv2a_stats.frame_working));
ImPlot::PopStyleColor(2);
ImGui::PopID();
}
ImPlot::EndPlot();
}
ImGui::TreePop();
}
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(2)) {
m_transparent = !m_transparent;
}
ImPlot::PopStyleVar(2);
}
ImGui::End();
ImGui::PopStyleColor(5);
}
DebugApuWindow apu_window;
DebugVideoWindow video_window;

40
ui/xui/debug.hh Normal file
View file

@ -0,0 +1,40 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
class DebugApuWindow
{
public:
bool m_is_open;
DebugApuWindow();
void Draw();
};
class DebugVideoWindow
{
public:
bool m_is_open;
bool m_transparent;
DebugVideoWindow();
void Draw();
};
extern DebugApuWindow apu_window;
extern DebugVideoWindow video_window;

118
ui/xui/font-manager.cc Normal file
View file

@ -0,0 +1,118 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "font-manager.hh"
#include "viewport-manager.hh"
#include "data/Roboto-Medium.ttf.h"
#include "data/RobotoCondensed-Regular.ttf.h"
#include "data/font_awesome_6_1_1_solid.otf.h"
#include "data/abxy.ttf.h"
FontManager g_font_mgr;
FontManager::FontManager()
{
m_last_viewport_scale = 1;
m_font_scale = 1;
}
void FontManager::Rebuild()
{
ImGuiIO &io = ImGui::GetIO();
// FIXME: Trim FA to only glyphs in use
io.Fonts->Clear();
{
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
m_default_font = io.Fonts->AddFontFromMemoryTTF(
(void *)Roboto_Medium_data, Roboto_Medium_size,
16.0f * g_viewport_mgr.m_scale * m_font_scale, &config);
m_menu_font_small = io.Fonts->AddFontFromMemoryTTF(
(void *)RobotoCondensed_Regular_data, RobotoCondensed_Regular_size,
22.0f * g_viewport_mgr.m_scale * m_font_scale, &config);
}
{
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
config.MergeMode = true;
config.GlyphOffset =
ImVec2(0, 13 * g_viewport_mgr.m_scale * m_font_scale);
config.GlyphMaxAdvanceX = 24.0f * g_viewport_mgr.m_scale * m_font_scale;
static const ImWchar icon_ranges[] = { 0xf900, 0xf903, 0 };
io.Fonts->AddFontFromMemoryTTF((void *)abxy_data, abxy_size,
40.0f * g_viewport_mgr.m_scale *
m_font_scale,
&config, icon_ranges);
}
{
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
m_menu_font_medium = io.Fonts->AddFontFromMemoryTTF(
(void *)RobotoCondensed_Regular_data, RobotoCondensed_Regular_size,
26.0f * g_viewport_mgr.m_scale * m_font_scale, &config);
m_menu_font = io.Fonts->AddFontFromMemoryTTF(
(void *)RobotoCondensed_Regular_data, RobotoCondensed_Regular_size,
34.0f * g_viewport_mgr.m_scale * m_font_scale, &config);
}
{
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
config.MergeMode = true;
config.GlyphOffset =
ImVec2(0, -3 * g_viewport_mgr.m_scale * m_font_scale);
config.GlyphMinAdvanceX = 32.0f * g_viewport_mgr.m_scale * m_font_scale;
static const ImWchar icon_ranges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
io.Fonts->AddFontFromMemoryTTF((void *)font_awesome_6_1_1_solid_data,
font_awesome_6_1_1_solid_size,
18.0f * g_viewport_mgr.m_scale *
m_font_scale,
&config, icon_ranges);
}
// {
// ImFontConfig config;
// config.FontDataOwnedByAtlas = false;
// static const ImWchar icon_ranges[] = { 0xf04c, 0xf04c, 0 };
// m_big_state_icon_font = io.Fonts->AddFontFromMemoryTTF(
// (void *)font_awesome_6_1_1_solid_data,
// font_awesome_6_1_1_solid_size,
// 64.0f * g_viewport_mgr.m_scale * m_font_scale, &config,
// icon_ranges);
// }
{
ImFontConfig config = ImFontConfig();
config.OversampleH = config.OversampleV = 1;
config.PixelSnapH = true;
config.SizePixels = 13.0f*g_viewport_mgr.m_scale;
m_fixed_width_font = io.Fonts->AddFontDefault(&config);
}
ImGui_ImplOpenGL3_CreateFontsTexture();
}
void FontManager::Update()
{
if (g_viewport_mgr.m_scale != m_last_viewport_scale) {
Rebuild();
m_last_viewport_scale = g_viewport_mgr.m_scale;
}
}

45
ui/xui/font-manager.hh Normal file
View file

@ -0,0 +1,45 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "common.hh"
#include "IconsFontAwesome6.h"
#define ICON_BUTTON_A "\xef\xa4\x80"
#define ICON_BUTTON_B "\xef\xa4\x81"
#define ICON_BUTTON_X "\xef\xa4\x82"
#define ICON_BUTTON_Y "\xef\xa4\x83"
class FontManager
{
public:
ImFont *m_default_font;
ImFont *m_fixed_width_font;
ImFont *m_menu_font;
ImFont *m_menu_font_small;
ImFont *m_menu_font_medium;
// ImFont *m_big_state_icon_font;
float m_last_viewport_scale;
float m_font_scale;
FontManager();
void Rebuild();
void Update();
};
extern FontManager g_font_mgr;

777
ui/xui/gl-helpers.cc Normal file
View file

@ -0,0 +1,777 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "common.hh"
#include <stdio.h>
#include <math.h>
#include <vector>
#include <fpng.h>
#include "gl-helpers.hh"
#include "stb_image.h"
#include "data/controller_mask.png.h"
#include "data/logo_sdf.png.h"
#include "ui/shader/xemu-logo-frag.h"
#include "notifications.hh"
Fbo *controller_fbo,
*logo_fbo;
GLuint g_controller_tex,
g_logo_tex;
enum ShaderType {
Blit,
BlitGamma, // FIMXE: Move to nv2a_get_framebuffer_surface
Mask,
Logo,
};
typedef struct DecalShader_
{
int flip;
float scale;
uint32_t time;
GLuint prog, vao, vbo, ebo;
GLint flipy_loc;
GLint tex_loc;
GLint scale_offset_loc;
GLint tex_scale_offset_loc;
GLint color_primary_loc;
GLint color_secondary_loc;
GLint color_fill_loc;
GLint time_loc;
GLint scale_loc;
GLint palette_loc[256];
} DecalShader;
static DecalShader *g_decal_shader,
*g_logo_shader,
*g_framebuffer_shader;
GLint Fbo::vp[4];
GLint Fbo::original_fbo;
bool Fbo::blend;
DecalShader *NewDecalShader(enum ShaderType type);
void DeleteDecalShader(DecalShader *s);
static GLint GetCurrentFbo()
{
GLint fbo;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, (GLint*)&fbo);
return fbo;
}
Fbo::Fbo(int width, int height)
{
w = width;
h = height;
// Allocate the texture
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA,
GL_UNSIGNED_BYTE, NULL);
GLint original = GetCurrentFbo();
// Allocate the framebuffer object
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
tex, 0);
GLenum DrawBuffers[1] = { GL_COLOR_ATTACHMENT0 };
glDrawBuffers(1, DrawBuffers);
glBindFramebuffer(GL_FRAMEBUFFER, original);
}
Fbo::~Fbo()
{
glDeleteTextures(1, &tex);
glDeleteFramebuffers(1, &fbo);
}
void Fbo::Target()
{
GLint vp[4];
glGetIntegerv(GL_VIEWPORT, vp);
original_fbo = GetCurrentFbo();
blend = glIsEnabled(GL_BLEND);
if (!blend) {
glEnable(GL_BLEND);
}
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, w, h);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
}
void Fbo::Restore()
{
if (!blend) {
glDisable(GL_BLEND);
}
// Restore default framebuffer, viewport, blending function
glBindFramebuffer(GL_FRAMEBUFFER, original_fbo);
glViewport(vp[0], vp[1], vp[2], vp[3]);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
static GLuint InitTexture(unsigned char *data, int width, int height,
int channels)
{
GLuint tex;
glGenTextures(1, &tex);
assert(tex != 0);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
return tex;
}
static GLuint LoadTextureFromMemory(const unsigned char *buf, unsigned int size)
{
// Flip vertically so textures are loaded according to GL convention.
stbi_set_flip_vertically_on_load(1);
int width, height, channels = 0;
unsigned char *data = stbi_load_from_memory(buf, size, &width, &height, &channels, 4);
assert(data != NULL);
GLuint tex = InitTexture(data, width, height, channels);
stbi_image_free(data);
return tex;
}
static GLuint Shader(GLenum type, const char *src)
{
char err_buf[512];
GLuint shader = glCreateShader(type);
assert(shader && "Failed to create shader");
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
glGetShaderInfoLog(shader, sizeof(err_buf), NULL, err_buf);
fprintf(stderr, "Shader compilation failed: %s\n\n"
"[Shader Source]\n"
"%s\n", err_buf, src);
assert(0);
}
return shader;
}
DecalShader *NewDecalShader(enum ShaderType type)
{
// Allocate shader wrapper object
DecalShader *s = new DecalShader;
assert(s != NULL);
s->flip = 0;
s->scale = 1.4;
s->time = 0;
const char *vert_src = R"(
#version 150 core
uniform bool in_FlipY;
uniform vec4 in_ScaleOffset;
uniform vec4 in_TexScaleOffset;
in vec2 in_Position;
in vec2 in_Texcoord;
out vec2 Texcoord;
void main() {
vec2 t = in_Texcoord;
if (in_FlipY) t.y = 1-t.y;
Texcoord = t*in_TexScaleOffset.xy + in_TexScaleOffset.zw;
gl_Position = vec4(in_Position*in_ScaleOffset.xy+in_ScaleOffset.zw, 0.0, 1.0);
}
)";
GLuint vert = Shader(GL_VERTEX_SHADER, vert_src);
assert(vert != 0);
// const char *image_frag_src = R"(
// #version 150 core
// uniform sampler2D tex;
// in vec2 Texcoord;
// out vec4 out_Color;
// void main() {
// out_Color.rgba = texture(tex, Texcoord);
// }
// )";
const char *image_gamma_frag_src = R"(
#version 400 core
uniform sampler2D tex;
uniform uint palette[256];
float gamma_ch(int ch, float col)
{
return float(bitfieldExtract(palette[uint(col * 255.0)], ch*8, 8)) / 255.0;
}
vec4 gamma(vec4 col)
{
return vec4(gamma_ch(0, col.r), gamma_ch(1, col.g), gamma_ch(2, col.b), col.a);
}
in vec2 Texcoord;
out vec4 out_Color;
void main() {
out_Color.rgba = gamma(texture(tex, Texcoord));
}
)";
// Simple 2-color decal shader
// - in_ColorFill is first pass
// - Red channel of the texture is used as primary color, mixed with 1-Red for
// secondary color.
// - Blue is a lazy alpha removal for now
// - Alpha channel passed through
const char *mask_frag_src = R"(
#version 150 core
uniform sampler2D tex;
uniform vec4 in_ColorPrimary;
uniform vec4 in_ColorSecondary;
uniform vec4 in_ColorFill;
in vec2 Texcoord;
out vec4 out_Color;
void main() {
vec4 t = texture(tex, Texcoord);
out_Color.rgba = in_ColorFill.rgba;
out_Color.rgb += mix(in_ColorSecondary.rgb, in_ColorPrimary.rgb, t.r);
out_Color.a += t.a - t.b;
}
)";
const char *frag_src = NULL;
switch (type) {
case ShaderType::Mask: frag_src = mask_frag_src; break;
// case ShaderType::Blit: frag_src = image_frag_src; break;
case ShaderType::BlitGamma: frag_src = image_gamma_frag_src; break;
case ShaderType::Logo: frag_src = xemu_logo_frag_src; break;
default: assert(0);
}
GLuint frag = Shader(GL_FRAGMENT_SHADER, frag_src);
assert(frag != 0);
// Link vertex and fragment shaders
s->prog = glCreateProgram();
glAttachShader(s->prog, vert);
glAttachShader(s->prog, frag);
glBindFragDataLocation(s->prog, 0, "out_Color");
glLinkProgram(s->prog);
glUseProgram(s->prog);
// Flag shaders for deletion when program is deleted
glDeleteShader(vert);
glDeleteShader(frag);
s->flipy_loc = glGetUniformLocation(s->prog, "in_FlipY");
s->scale_offset_loc = glGetUniformLocation(s->prog, "in_ScaleOffset");
s->tex_scale_offset_loc =
glGetUniformLocation(s->prog, "in_TexScaleOffset");
s->tex_loc = glGetUniformLocation(s->prog, "tex");
s->color_primary_loc = glGetUniformLocation(s->prog, "in_ColorPrimary");
s->color_secondary_loc = glGetUniformLocation(s->prog, "in_ColorSecondary");
s->color_fill_loc = glGetUniformLocation(s->prog, "in_ColorFill");
s->time_loc = glGetUniformLocation(s->prog, "iTime");
s->scale_loc = glGetUniformLocation(s->prog, "scale");
for (int i = 0; i < 256; i++) {
char name[64];
snprintf(name, sizeof(name), "palette[%d]", i);
s->palette_loc[i] = glGetUniformLocation(s->prog, name);
}
const GLfloat verts[6][4] = {
// x y s t
{ -1.0f, -1.0f, 0.0f, 0.0f }, // BL
{ -1.0f, 1.0f, 0.0f, 1.0f }, // TL
{ 1.0f, 1.0f, 1.0f, 1.0f }, // TR
{ 1.0f, -1.0f, 1.0f, 0.0f }, // BR
};
const GLint indicies[] = { 0, 1, 2, 3 };
glGenVertexArrays(1, &s->vao);
glBindVertexArray(s->vao);
glGenBuffers(1, &s->vbo);
glBindBuffer(GL_ARRAY_BUFFER, s->vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_COPY);
glGenBuffers(1, &s->ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicies), indicies, GL_STATIC_DRAW);
GLint loc = glGetAttribLocation(s->prog, "in_Position");
if (loc >= 0) {
glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(loc);
}
loc = glGetAttribLocation(s->prog, "in_Texcoord");
if (loc >= 0) {
glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)(2*sizeof(GLfloat)));
glEnableVertexAttribArray(loc);
}
return s;
}
void RenderDecal(DecalShader *s, float x, float y, float w, float h,
float tex_x, float tex_y, float tex_w, float tex_h,
uint32_t primary, uint32_t secondary, uint32_t fill)
{
GLint vp[4];
glGetIntegerv(GL_VIEWPORT, vp);
float ww = vp[2], wh = vp[3];
x = (int)x;
y = (int)y;
w = (int)w;
h = (int)h;
tex_x = (int)tex_x;
tex_y = (int)tex_y;
tex_w = (int)tex_w;
tex_h = (int)tex_h;
int tw_i, th_i;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw_i);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th_i);
float tw = tw_i, th = th_i;
#define COL(color, c) (float)(((color)>>((c)*8)) & 0xff)/255.0
glUniform1i(s->flipy_loc, s->flip);
glUniform4f(s->scale_offset_loc, w / ww, h / wh, -1 + ((2 * x + w) / ww),
-1 + ((2 * y + h) / wh));
glUniform4f(s->tex_scale_offset_loc, tex_w / tw, tex_h / th, tex_x / tw,
tex_y / th);
glUniform1i(s->tex_loc, 0);
glUniform4f(s->color_primary_loc, COL(primary, 3), COL(primary, 2),
COL(primary, 1), COL(primary, 0));
glUniform4f(s->color_secondary_loc, COL(secondary, 3), COL(secondary, 2),
COL(secondary, 1), COL(secondary, 0));
glUniform4f(s->color_fill_loc, COL(fill, 3), COL(fill, 2), COL(fill, 1),
COL(fill, 0));
if (s->time_loc >= 0) glUniform1f(s->time_loc, s->time/1000.0f);
if (s->scale_loc >= 0) glUniform1f(s->scale_loc, s->scale);
#undef COL
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL);
}
struct rect {
int x, y, w, h;
};
static const struct rect tex_items[] = {
{ 0, 148, 467, 364 }, // obj_controller
{ 0, 81, 67, 67 }, // obj_lstick
{ 0, 14, 67, 67 }, // obj_rstick
{ 67, 104, 68, 44 }, // obj_port_socket
{ 67, 76, 28, 28 }, // obj_port_lbl_1
{ 67, 48, 28, 28 }, // obj_port_lbl_2
{ 67, 20, 28, 28 }, // obj_port_lbl_3
{ 95, 76, 28, 28 }, // obj_port_lbl_4
};
enum tex_item_names {
obj_controller,
obj_lstick,
obj_rstick,
obj_port_socket,
obj_port_lbl_1,
obj_port_lbl_2,
obj_port_lbl_3,
obj_port_lbl_4,
};
void InitCustomRendering(void)
{
glActiveTexture(GL_TEXTURE0);
g_controller_tex =
LoadTextureFromMemory(controller_mask_data, controller_mask_size);
g_decal_shader = NewDecalShader(ShaderType::Mask);
controller_fbo = new Fbo(512, 512);
g_logo_tex = LoadTextureFromMemory(logo_sdf_data, logo_sdf_size);
g_logo_shader = NewDecalShader(ShaderType::Logo);
logo_fbo = new Fbo(512, 512);
g_framebuffer_shader = NewDecalShader(ShaderType::BlitGamma);
}
static void RenderMeter(DecalShader *s, float x, float y, float width,
float height, float p, uint32_t color_bg,
uint32_t color_fg)
{
RenderDecal(s, x, y, width, height, 0, 0, 1, 1, 0, 0, color_bg);
RenderDecal(s, x, y, width * p, height, 0, 0, 1, 1, 0, 0, color_fg);
}
void RenderController(float frame_x, float frame_y, uint32_t primary_color,
uint32_t secondary_color, ControllerState *state)
{
// Location within the controller texture of masked button locations,
// relative to the origin of the controller
const struct rect jewel = { 177, 172, 113, 118 };
const struct rect lstick_ctr = { 93, 246, 0, 0 };
const struct rect rstick_ctr = { 342, 148, 0, 0 };
const struct rect buttons[12] = {
{ 367, 187, 30, 38 }, // A
{ 368, 229, 30, 38 }, // B
{ 330, 204, 30, 38 }, // X
{ 331, 247, 30, 38 }, // Y
{ 82, 121, 31, 47 }, // D-Left
{ 104, 160, 44, 25 }, // D-Up
{ 141, 121, 31, 47 }, // D-Right
{ 104, 105, 44, 25 }, // D-Down
{ 187, 94, 34, 24 }, // Back
{ 246, 94, 36, 26 }, // Start
{ 348, 288, 30, 38 }, // White
{ 386, 268, 30, 38 }, // Black
};
uint8_t alpha = 0;
uint32_t now = SDL_GetTicks();
float t;
glUseProgram(g_decal_shader->prog);
glBindVertexArray(g_decal_shader->vao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_controller_tex);
// Add a 5 pixel space around the controller so we can wiggle the controller
// around to visualize rumble in action
frame_x += 5;
frame_y += 5;
float original_frame_x = frame_x;
float original_frame_y = frame_y;
// Floating point versions that will get scaled
float rumble_l = 0;
float rumble_r = 0;
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ZERO);
uint32_t jewel_color = secondary_color;
// Check to see if the guide button is pressed
const uint32_t animate_guide_button_duration = 2000;
if (state->buttons & CONTROLLER_BUTTON_GUIDE) {
state->animate_guide_button_end = now + animate_guide_button_duration;
}
if (now < state->animate_guide_button_end) {
t = 1.0f - (float)(state->animate_guide_button_end-now)/(float)animate_guide_button_duration;
float sin_wav = (1-sin(M_PI * t / 2.0f));
// Animate guide button by highlighting logo jewel and fading out over time
alpha = sin_wav * 255.0f;
jewel_color = primary_color + alpha;
// Add a little extra flare: wiggle the frame around while we rumble
frame_x += ((float)(rand() % 5)-2.5) * (1-t);
frame_y += ((float)(rand() % 5)-2.5) * (1-t);
rumble_l = rumble_r = sin_wav;
}
// Render controller texture
RenderDecal(g_decal_shader, frame_x + 0, frame_y + 0,
tex_items[obj_controller].w, tex_items[obj_controller].h,
tex_items[obj_controller].x, tex_items[obj_controller].y,
tex_items[obj_controller].w, tex_items[obj_controller].h,
primary_color, secondary_color, 0);
glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE); // Blend with controller cutouts
RenderDecal(g_decal_shader, frame_x + jewel.x, frame_y + jewel.y, jewel.w,
jewel.h, 0, 0, 1, 1, 0, 0, jewel_color);
// The controller has alpha cutouts where the buttons are. Draw a surface
// behind the buttons if they are activated
for (int i = 0; i < 12; i++) {
if (state->buttons & (1 << i)) {
RenderDecal(g_decal_shader, frame_x + buttons[i].x,
frame_y + buttons[i].y, buttons[i].w, buttons[i].h, 0,
0, 1, 1, 0, 0, primary_color + 0xff);
}
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Blend with controller
// Render left thumbstick
float w = tex_items[obj_lstick].w;
float h = tex_items[obj_lstick].h;
float c_x = frame_x+lstick_ctr.x;
float c_y = frame_y+lstick_ctr.y;
float lstick_x = (float)state->axis[CONTROLLER_AXIS_LSTICK_X]/32768.0;
float lstick_y = (float)state->axis[CONTROLLER_AXIS_LSTICK_Y]/32768.0;
RenderDecal(g_decal_shader, (int)(c_x - w / 2.0f + 10.0f * lstick_x),
(int)(c_y - h / 2.0f + 10.0f * lstick_y), w, h,
tex_items[obj_lstick].x, tex_items[obj_lstick].y, w, h,
(state->buttons & CONTROLLER_BUTTON_LSTICK) ? secondary_color :
primary_color,
(state->buttons & CONTROLLER_BUTTON_LSTICK) ? primary_color :
secondary_color,
0);
// Render right thumbstick
w = tex_items[obj_rstick].w;
h = tex_items[obj_rstick].h;
c_x = frame_x+rstick_ctr.x;
c_y = frame_y+rstick_ctr.y;
float rstick_x = (float)state->axis[CONTROLLER_AXIS_RSTICK_X]/32768.0;
float rstick_y = (float)state->axis[CONTROLLER_AXIS_RSTICK_Y]/32768.0;
RenderDecal(g_decal_shader, (int)(c_x - w / 2.0f + 10.0f * rstick_x),
(int)(c_y - h / 2.0f + 10.0f * rstick_y), w, h,
tex_items[obj_rstick].x, tex_items[obj_rstick].y, w, h,
(state->buttons & CONTROLLER_BUTTON_RSTICK) ? secondary_color :
primary_color,
(state->buttons & CONTROLLER_BUTTON_RSTICK) ? primary_color :
secondary_color,
0);
glBlendFunc(GL_ONE, GL_ZERO); // Don't blend, just overwrite values in buffer
// Render trigger bars
float ltrig = state->axis[CONTROLLER_AXIS_LTRIG] / 32767.0;
float rtrig = state->axis[CONTROLLER_AXIS_RTRIG] / 32767.0;
const uint32_t animate_trigger_duration = 1000;
if ((ltrig > 0) || (rtrig > 0)) {
state->animate_trigger_end = now + animate_trigger_duration;
rumble_l = fmax(rumble_l, ltrig);
rumble_r = fmax(rumble_r, rtrig);
}
// Animate trigger alpha down after a period of inactivity
alpha = 0x80;
if (state->animate_trigger_end > now) {
t = 1.0f - (float)(state->animate_trigger_end-now)/(float)animate_trigger_duration;
float sin_wav = (1-sin(M_PI * t / 2.0f));
alpha += fmin(sin_wav * 0x40, 0x80);
}
RenderMeter(g_decal_shader, original_frame_x + 10,
original_frame_y + tex_items[obj_controller].h + 20, 150, 5,
ltrig, primary_color + alpha, primary_color + 0xff);
RenderMeter(g_decal_shader,
original_frame_x + tex_items[obj_controller].w - 160,
original_frame_y + tex_items[obj_controller].h + 20, 150, 5,
rtrig, primary_color + alpha, primary_color + 0xff);
// Apply rumble updates
state->rumble_l = (int)(rumble_l * (float)0xffff);
state->rumble_r = (int)(rumble_r * (float)0xffff);
glBindVertexArray(0);
glUseProgram(0);
}
void RenderControllerPort(float frame_x, float frame_y, int i,
uint32_t port_color)
{
glUseProgram(g_decal_shader->prog);
glBindVertexArray(g_decal_shader->vao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_controller_tex);
glBlendFunc(GL_ONE, GL_ZERO);
// Render port socket
RenderDecal(g_decal_shader, frame_x, frame_y, tex_items[obj_port_socket].w,
tex_items[obj_port_socket].h, tex_items[obj_port_socket].x,
tex_items[obj_port_socket].y, tex_items[obj_port_socket].w,
tex_items[obj_port_socket].h, port_color, port_color, 0);
frame_x += (tex_items[obj_port_socket].w-tex_items[obj_port_lbl_1].w)/2;
frame_y += tex_items[obj_port_socket].h + 8;
// Render port label
RenderDecal(
g_decal_shader, frame_x, frame_y, tex_items[obj_port_lbl_1 + i].w,
tex_items[obj_port_lbl_1 + i].h, tex_items[obj_port_lbl_1 + i].x,
tex_items[obj_port_lbl_1 + i].y, tex_items[obj_port_lbl_1 + i].w,
tex_items[obj_port_lbl_1 + i].h, port_color, port_color, 0);
glBindVertexArray(0);
glUseProgram(0);
}
void RenderLogo(uint32_t time, uint32_t primary_color, uint32_t secondary_color,
uint32_t fill_color)
{
g_logo_shader->time = time;
glUseProgram(g_logo_shader->prog);
glBindVertexArray(g_decal_shader->vao);
glBlendFunc(GL_ONE, GL_ZERO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, g_logo_tex);
RenderDecal(g_logo_shader, 0, 0, 512, 512, 0, 0, 128, 128, primary_color,
secondary_color, fill_color);
glBindVertexArray(0);
glUseProgram(0);
}
void RenderFramebuffer(GLint tex, int width, int height, bool flip)
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex);
int tw, th;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th);
// Calculate scaling factors
float scale[2];
if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) {
// Stretch to fit
scale[0] = 1.0;
scale[1] = 1.0;
} else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_CENTER) {
// Centered
scale[0] = (float)tw/(float)width;
scale[1] = (float)th/(float)height;
} else {
float t_ratio;
if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) {
// Scale to fit window using a fixed 16:9 aspect ratio
t_ratio = 16.0f/9.0f;
} else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) {
t_ratio = 4.0f/3.0f;
} else {
// Scale to fit, preserving framebuffer aspect ratio
t_ratio = (float)tw/(float)th;
}
float w_ratio = (float)width/(float)height;
if (w_ratio >= t_ratio) {
scale[0] = t_ratio/w_ratio;
scale[1] = 1.0;
} else {
scale[0] = 1.0;
scale[1] = w_ratio/t_ratio;
}
}
DecalShader *s = g_framebuffer_shader;
s->flip = flip;
glViewport(0, 0, width, height);
glUseProgram(s->prog);
glBindVertexArray(s->vao);
glUniform1i(s->flipy_loc, s->flip);
glUniform4f(s->scale_offset_loc, scale[0], scale[1], 0, 0);
glUniform4f(s->tex_scale_offset_loc, 1.0, 1.0, 0, 0);
glUniform1i(s->tex_loc, 0);
const uint8_t *palette = nv2a_get_dac_palette();
for (int i = 0; i < 256; i++) {
uint32_t e = (palette[i * 3 + 2] << 16) | (palette[i * 3 + 1] << 8) |
palette[i * 3];
glUniform1ui(s->palette_loc[i], e);
}
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL);
}
void SaveScreenshot(GLuint tex, bool flip)
{
int width, height;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
glBindTexture(GL_TEXTURE_2D, 0);
if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) {
width = height * (16.0f / 9.0f);
} else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) {
width = height * (4.0f / 3.0f);
}
std::vector<uint8_t> pixels;
pixels.resize(width * height * 4);
Fbo fbo(width, height);
fbo.Target();
bool blend = glIsEnabled(GL_BLEND);
if (blend) glDisable(GL_BLEND);
RenderFramebuffer(tex, width, height, !flip);
if (blend) glEnable(GL_BLEND);
glPixelStorei(GL_PACK_ROW_LENGTH, width);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, height);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadnPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.size(),
pixels.data());
fbo.Restore();
char fname[128];
Error *err = NULL;
std::vector<uint8_t> png;
if (fpng::fpng_encode_image_to_memory(pixels.data(), width, height, 3, png)) {
time_t t = time(NULL);
struct tm *tmp = localtime(&t);
if (tmp) {
strftime(fname, sizeof(fname), "xemu-%Y-%m-%d-%H-%M-%S.png",
tmp);
} else {
strcpy(fname, "xemu.png");
}
const char *output_dir = g_config.general.screenshot_dir;
if (!strlen(output_dir)) {
output_dir = ".";
}
// FIXME: Check for existing path
char *path = g_strdup_printf("%s/%s", output_dir, fname);
FILE *fd = qemu_fopen(path, "wb");
if (fd) {
int s = fwrite(png.data(), png.size(), 1, fd);
if (s != 1) {
error_setg(&err, "Failed to write %s", path);
}
fclose(fd);
} else {
error_setg(&err, "Failed to open %s for writing", path);
}
g_free(path);
} else {
error_setg(&err, "Failed to encode PNG image");
}
if (err) {
xemu_queue_error_message(error_get_pretty(err));
error_report_err(err);
} else {
char *msg = g_strdup_printf("Screenshot Saved: %s", fname);
xemu_queue_notification(msg);
free(msg);
}
}

50
ui/xui/gl-helpers.hh Normal file
View file

@ -0,0 +1,50 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "common.hh"
#include "../xemu-input.h"
class Fbo
{
public:
static GLint vp[4];
static GLint original_fbo;
static bool blend;
int w, h;
GLuint fbo, tex;
Fbo(int width, int height);
~Fbo();
inline GLuint Texture() { return tex; }
void Target();
void Restore();
};
extern Fbo *controller_fbo, *logo_fbo;
void InitCustomRendering(void);
void RenderLogo(uint32_t time, uint32_t primary_color, uint32_t secondary_color,
uint32_t fill_color);
void RenderController(float frame_x, float frame_y, uint32_t primary_color,
uint32_t secondary_color, ControllerState *state);
void RenderControllerPort(float frame_x, float frame_y, int i,
uint32_t port_color);
void RenderFramebuffer(GLint tex, int width, int height, bool flip);
void SaveScreenshot(GLuint tex, bool flip);

116
ui/xui/input-manager.cc Normal file
View file

@ -0,0 +1,116 @@
#include "common.hh"
#include "input-manager.hh"
#include "../xemu-input.h"
InputManager g_input_mgr;
InputManager::InputManager()
{
m_last_mouse_pos = ImVec2(0, 0);
m_navigating_with_controller = false;
}
void InputManager::Update()
{
ImGuiIO& io = ImGui::GetIO();
// Combine all controller states to allow any controller to navigate
m_buttons = 0;
int16_t axis[CONTROLLER_AXIS__COUNT] = {0};
ControllerState *iter;
QTAILQ_FOREACH(iter, &available_controllers, entry) {
if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) continue;
m_buttons |= iter->buttons;
// We simply take any axis that is >10 % activation
for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) {
if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) {
axis[i] = iter->axis[i];
}
}
}
// If the mouse is moved, wake the ui
ImVec2 current_mouse_pos = ImGui::GetMousePos();
m_mouse_moved = false;
if ((current_mouse_pos.x != m_last_mouse_pos.x) ||
(current_mouse_pos.y != m_last_mouse_pos.y) ||
ImGui::IsMouseDown(0) || ImGui::IsMouseDown(1) || ImGui::IsMouseDown(2)) {
m_mouse_moved = true;
m_last_mouse_pos = current_mouse_pos;
m_navigating_with_controller = false;
}
// If mouse capturing is enabled (we are in a dialog), ensure the UI is alive
bool controller_focus_capture = false;
if (io.NavActive) {
controller_focus_capture = true;
m_navigating_with_controller |= !!m_buttons;
}
// Prevent controller events from going to the guest if they are being used
// to navigate the HUD
xemu_input_set_test_mode(controller_focus_capture); // FIXME: Rename 'test mode'
// Update gamepad inputs
#define IM_SATURATE(V) (V < 0.0f ? 0.0f : V > 1.0f ? 1.0f : V)
#define MAP_BUTTON(KEY_NO, BUTTON_NO) { io.AddKeyEvent(KEY_NO, !!(m_buttons & BUTTON_NO)); }
#define MAP_ANALOG(KEY_NO, AXIS_NO, V0, V1) { float vn = (float)(axis[AXIS_NO] - V0) / (float)(V1 - V0); vn = IM_SATURATE(vn); io.AddKeyAnalogEvent(KEY_NO, vn > 0.1f, vn); }
const int thumb_dead_zone = 8000; // SDL_gamecontroller.h suggests using this value.
MAP_BUTTON(ImGuiKey_GamepadStart, CONTROLLER_BUTTON_START);
MAP_BUTTON(ImGuiKey_GamepadBack, CONTROLLER_BUTTON_BACK);
MAP_BUTTON(ImGuiKey_GamepadFaceDown, CONTROLLER_BUTTON_A); // Xbox A, PS Cross
MAP_BUTTON(ImGuiKey_GamepadFaceRight, CONTROLLER_BUTTON_B); // Xbox B, PS Circle
MAP_BUTTON(ImGuiKey_GamepadFaceLeft, CONTROLLER_BUTTON_X); // Xbox X, PS Square
MAP_BUTTON(ImGuiKey_GamepadFaceUp, CONTROLLER_BUTTON_Y); // Xbox Y, PS Triangle
MAP_BUTTON(ImGuiKey_GamepadDpadLeft, CONTROLLER_BUTTON_DPAD_LEFT);
MAP_BUTTON(ImGuiKey_GamepadDpadRight, CONTROLLER_BUTTON_DPAD_RIGHT);
MAP_BUTTON(ImGuiKey_GamepadDpadUp, CONTROLLER_BUTTON_DPAD_UP);
MAP_BUTTON(ImGuiKey_GamepadDpadDown, CONTROLLER_BUTTON_DPAD_DOWN);
MAP_BUTTON(ImGuiKey_GamepadL1, CONTROLLER_BUTTON_WHITE);
MAP_BUTTON(ImGuiKey_GamepadR1, CONTROLLER_BUTTON_BLACK);
//MAP_ANALOG(ImGuiKey_GamepadL2, SDL_CONTROLLER_AXIS_TRIGGERLEFT, 0.0f, 32767);
//MAP_ANALOG(ImGuiKey_GamepadR2, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, 0.0f, 32767);
//MAP_BUTTON(ImGuiKey_GamepadL3, SDL_CONTROLLER_BUTTON_LEFTSTICK);
//MAP_BUTTON(ImGuiKey_GamepadR3, SDL_CONTROLLER_BUTTON_RIGHTSTICK);
MAP_ANALOG(ImGuiKey_GamepadLStickLeft, CONTROLLER_AXIS_LSTICK_X, -thumb_dead_zone, -32768);
MAP_ANALOG(ImGuiKey_GamepadLStickRight, CONTROLLER_AXIS_LSTICK_X, +thumb_dead_zone, +32767);
MAP_ANALOG(ImGuiKey_GamepadLStickUp, CONTROLLER_AXIS_LSTICK_Y, +thumb_dead_zone, +32768);
MAP_ANALOG(ImGuiKey_GamepadLStickDown, CONTROLLER_AXIS_LSTICK_Y, -thumb_dead_zone, -32767);
MAP_ANALOG(ImGuiKey_GamepadRStickLeft, CONTROLLER_AXIS_RSTICK_X, -thumb_dead_zone, -32768);
MAP_ANALOG(ImGuiKey_GamepadRStickRight, CONTROLLER_AXIS_RSTICK_X, +thumb_dead_zone, +32767);
MAP_ANALOG(ImGuiKey_GamepadRStickUp, CONTROLLER_AXIS_RSTICK_Y, +thumb_dead_zone, +32768);
MAP_ANALOG(ImGuiKey_GamepadRStickDown, CONTROLLER_AXIS_RSTICK_Y, -thumb_dead_zone, -32767);
#undef MAP_BUTTON
#undef MAP_ANALOG
#undef IM_SATURATE
io.BackendUsingLegacyNavInputArray = true;
// Map to nav inputs
#define NAV_MAP_KEY(_KEY, _NAV_INPUT, _ACTIVATE_NAV) \
do { \
io.NavInputs[_NAV_INPUT] = io.KeysData[_KEY - ImGuiKey_KeysData_OFFSET].AnalogValue; \
if (_ACTIVATE_NAV && io.NavInputs[_NAV_INPUT] > 0.0f) { \
ImGui::GetCurrentContext()->NavInputSource = ImGuiInputSource_Gamepad; \
} \
} while (0)
NAV_MAP_KEY(ImGuiKey_GamepadFaceDown, ImGuiNavInput_Activate, true);
NAV_MAP_KEY(ImGuiKey_GamepadFaceRight, ImGuiNavInput_Cancel, true);
//NAV_MAP_KEY(ImGuiKey_Menu, ImGuiNavInput_Menu, true);
NAV_MAP_KEY(ImGuiKey_GamepadFaceUp, ImGuiNavInput_Input, true);
NAV_MAP_KEY(ImGuiKey_GamepadDpadLeft, ImGuiNavInput_DpadLeft, true);
NAV_MAP_KEY(ImGuiKey_GamepadDpadRight, ImGuiNavInput_DpadRight, true);
NAV_MAP_KEY(ImGuiKey_GamepadDpadUp, ImGuiNavInput_DpadUp, true);
NAV_MAP_KEY(ImGuiKey_GamepadDpadDown, ImGuiNavInput_DpadDown, true);
NAV_MAP_KEY(ImGuiKey_GamepadL1, ImGuiNavInput_FocusPrev, false);
NAV_MAP_KEY(ImGuiKey_GamepadR1, ImGuiNavInput_FocusNext, false);
NAV_MAP_KEY(ImGuiKey_GamepadL1, ImGuiNavInput_TweakSlow, false);
NAV_MAP_KEY(ImGuiKey_GamepadR1, ImGuiNavInput_TweakFast, false);
NAV_MAP_KEY(ImGuiKey_GamepadLStickLeft, ImGuiNavInput_LStickLeft, false);
NAV_MAP_KEY(ImGuiKey_GamepadLStickRight, ImGuiNavInput_LStickRight, false);
NAV_MAP_KEY(ImGuiKey_GamepadLStickUp, ImGuiNavInput_LStickUp, false);
NAV_MAP_KEY(ImGuiKey_GamepadLStickDown, ImGuiNavInput_LStickDown, false);
#undef NAV_MAP_KEY
}

20
ui/xui/input-manager.hh Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include "common.hh"
class InputManager
{
protected:
ImVec2 m_last_mouse_pos;
bool m_navigating_with_controller;
uint32_t m_buttons;
bool m_mouse_moved;
public:
InputManager();
void Update();
inline bool IsNavigatingWithController() { return m_navigating_with_controller; }
inline bool MouseMoved() { return m_mouse_moved; }
inline uint32_t CombinedButtons() { return m_buttons; }
};
extern InputManager g_input_mgr;

1142
ui/xui/main-menu.cc Normal file

File diff suppressed because it is too large Load diff

187
ui/xui/main-menu.hh Normal file
View file

@ -0,0 +1,187 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <string>
#include <vector>
#include <memory>
#include "common.hh"
#include "widgets.hh"
#include "scene.hh"
#include "scene-components.hh"
extern "C" {
#include "net/pcap.h"
#undef snprintf // FIXME
}
class MainMenuTabView
{
public:
virtual ~MainMenuTabView();
virtual void Draw();
};
class MainMenuGeneralView : public virtual MainMenuTabView
{
public:
void Draw() override;
};
class MainMenuInputView : public virtual MainMenuTabView
{
public:
void Draw() override;
};
class MainMenuDisplayView : public virtual MainMenuTabView
{
public:
void Draw() override;
};
class MainMenuAudioView : public virtual MainMenuTabView
{
public:
void Draw() override;
};
class NetworkInterface
{
public:
std::string m_pcap_name;
std::string m_description;
std::string m_friendly_name;
NetworkInterface(pcap_if_t *pcap_desc, char *_friendlyname = NULL);
};
class NetworkInterfaceManager
{
public:
std::vector<std::unique_ptr<NetworkInterface>> m_ifaces;
NetworkInterface *m_current_iface;
bool m_failed_to_load_lib;
NetworkInterfaceManager();
void Refresh(void);
void Select(NetworkInterface &iface);
bool IsCurrent(NetworkInterface &iface);
};
class MainMenuNetworkView : public virtual MainMenuTabView
{
protected:
char remote_addr[64];
char local_addr[64];
bool should_refresh;
std::unique_ptr<NetworkInterfaceManager> iface_mgr;
public:
MainMenuNetworkView();
void Draw() override;
void DrawPcapOptions(bool appearing);
void DrawNatOptions(bool appearing);
void DrawUdpOptions(bool appearing);
};
class MainMenuSnapshotsView : public virtual MainMenuTabView
{
public:
void SnapshotBigButton(const char *name, const char *title_name,
GLuint screenshot);
void Draw() override;
};
class MainMenuSystemView : public virtual MainMenuTabView
{
protected:
bool m_dirty;
public:
MainMenuSystemView();
void Draw() override;
};
class MainMenuAboutView : public virtual MainMenuTabView
{
public:
void Draw() override;
};
class MainMenuTabButton
{
protected:
std::string m_icon;
std::string m_text;
MainMenuTabView *m_view;
public:
MainMenuTabButton(std::string text, std::string icon = "", MainMenuTabView *view = nullptr);
MainMenuTabView *view();
bool Draw(bool selected);
};
class MainMenuScene : public virtual Scene {
protected:
EasingAnimation m_animation;
bool m_focus_view;
bool m_had_focus_last_frame;
int m_current_view_index;
int m_next_view_index;
BackgroundGradient m_background;
NavControlAnnotation m_nav_control_view;
std::vector<MainMenuTabButton*> m_tabs;
MainMenuTabButton m_general_button,
m_input_button,
m_display_button,
m_audio_button,
m_network_button,
// m_snapshots_button,
m_system_button,
m_about_button;
MainMenuGeneralView m_general_view;
MainMenuInputView m_input_view;
MainMenuDisplayView m_display_view;
MainMenuAudioView m_audio_view;
MainMenuNetworkView m_network_view;
// MainMenuSnapshotsView m_snapshots_view;
MainMenuSystemView m_system_view;
MainMenuAboutView m_about_view;
public:
MainMenuScene();
void ShowGeneral();
void ShowInput();
void ShowDisplay();
void ShowAudio();
void ShowNetwork();
// void ShowSnapshots();
void ShowSystem();
void ShowAbout();
void SetNextViewIndexWithFocus(int i);
void Show() override;
void Hide() override;
bool IsAnimating() override;
void SetNextViewIndex(int i);
void HandleInput();
bool Draw() override;
};
extern MainMenuScene g_main_menu;

306
ui/xui/main.cc Normal file
View file

@ -0,0 +1,306 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include <SDL.h>
#include <epoxy/gl.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <functional>
#include <assert.h>
#include <fpng.h>
#include <deque>
#include <vector>
#include <string>
#include <memory>
#include "common.hh"
#include "xemu-hud.h"
#include "misc.hh"
#include "gl-helpers.hh"
#include "input-manager.hh"
#include "viewport-manager.hh"
#include "font-manager.hh"
#include "scene.hh"
#include "scene-manager.hh"
#include "main-menu.hh"
#include "popup-menu.hh"
#include "notifications.hh"
#include "monitor.hh"
#include "debug.hh"
#include "welcome.hh"
#include "menubar.hh"
#include "compat.hh"
#if defined(_WIN32)
#include "update.hh"
#endif
bool g_screenshot_pending;
float g_main_menu_height;
static ImGuiStyle g_base_style;
static SDL_Window *g_sdl_window;
static float g_last_scale;
static int g_vsync;
static GLuint g_tex;
static bool g_flip_req;
static void InitializeStyle()
{
g_font_mgr.Rebuild();
ImGui::StyleColorsDark();
ImVec4 *c = ImGui::GetStyle().Colors;
c[ImGuiCol_Text] = ImVec4(0.94f, 0.94f, 0.94f, 1.00f);
c[ImGuiCol_TextDisabled] = ImVec4(0.86f, 0.93f, 0.89f, 0.28f);
c[ImGuiCol_WindowBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f);
c[ImGuiCol_ChildBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.98f);
c[ImGuiCol_PopupBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f);
c[ImGuiCol_Border] = ImVec4(0.11f, 0.11f, 0.11f, 0.60f);
c[ImGuiCol_BorderShadow] = ImVec4(0.16f, 0.16f, 0.16f, 0.00f);
c[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.18f, 0.18f, 1.00f);
c[ImGuiCol_FrameBgHovered] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f);
c[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
c[ImGuiCol_TitleBg] = ImVec4(0.20f, 0.51f, 0.18f, 1.00f);
c[ImGuiCol_TitleBgActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.16f, 0.16f, 0.16f, 0.75f);
c[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.00f);
c[ImGuiCol_ScrollbarBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.00f);
c[ImGuiCol_ScrollbarGrab] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f);
c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
c[ImGuiCol_SliderGrab] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
c[ImGuiCol_SliderGrabActive] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
c[ImGuiCol_Button] = ImVec4(0.17f, 0.17f, 0.17f, 1.00f);
c[ImGuiCol_ButtonHovered] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
c[ImGuiCol_Header] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_Separator] = ImVec4(1.00f, 1.00f, 1.00f, 0.25f);
c[ImGuiCol_SeparatorHovered] = ImVec4(0.13f, 0.87f, 0.16f, 0.78f);
c[ImGuiCol_SeparatorActive] = ImVec4(0.25f, 0.75f, 0.10f, 1.00f);
c[ImGuiCol_ResizeGrip] = ImVec4(0.47f, 0.83f, 0.49f, 0.04f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
c[ImGuiCol_ResizeGripActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
c[ImGuiCol_Tab] = ImVec4(0.26f, 0.67f, 0.23f, 0.95f);
c[ImGuiCol_TabHovered] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_TabActive] = ImVec4(0.24f, 0.60f, 0.00f, 1.00f);
c[ImGuiCol_TabUnfocused] = ImVec4(0.21f, 0.54f, 0.19f, 0.99f);
c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.24f, 0.60f, 0.21f, 1.00f);
c[ImGuiCol_PlotLines] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
c[ImGuiCol_PlotLinesHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
c[ImGuiCol_PlotHistogram] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
c[ImGuiCol_PlotHistogramHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
c[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
c[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
c[ImGuiCol_NavHighlight] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
c[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.73f);
ImGuiStyle &s = ImGui::GetStyle();
s.WindowRounding = 6.0;
s.FrameRounding = 6.0;
s.PopupRounding = 6.0;
g_base_style = s;
}
void xemu_hud_init(SDL_Window* window, void* sdl_gl_context)
{
xemu_monitor_init();
g_vsync = g_config.display.window.vsync;
InitCustomRendering();
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.IniFilename = NULL;
// Setup Platform/Renderer bindings
ImGui_ImplSDL2_InitForOpenGL(window, sdl_gl_context);
ImGui_ImplOpenGL3_Init("#version 150");
g_sdl_window = window;
ImPlot::CreateContext();
#if defined(_WIN32)
if (!g_config.general.show_welcome && g_config.general.updates.check) {
update_window.CheckForUpdates();
}
#endif
g_last_scale = g_viewport_mgr.m_scale;
InitializeStyle();
g_main_menu.SetNextViewIndex(g_config.general.last_viewed_menu_index);
first_boot_window.is_open = g_config.general.show_welcome;
}
void xemu_hud_cleanup(void)
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
}
void xemu_hud_process_sdl_events(SDL_Event *event)
{
ImGui_ImplSDL2_ProcessEvent(event);
}
void xemu_hud_should_capture_kbd_mouse(int *kbd, int *mouse)
{
ImGuiIO& io = ImGui::GetIO();
if (kbd) *kbd = io.WantCaptureKeyboard;
if (mouse) *mouse = io.WantCaptureMouse;
}
void xemu_hud_set_framebuffer_texture(GLuint tex, bool flip)
{
g_tex = tex;
g_flip_req = flip;
}
void xemu_hud_render(void)
{
ImGuiIO& io = ImGui::GetIO();
uint32_t now = SDL_GetTicks();
g_viewport_mgr.Update();
g_font_mgr.Update();
if (g_last_scale != g_viewport_mgr.m_scale) {
ImGuiStyle &style = ImGui::GetStyle();
style = g_base_style;
style.ScaleAllSizes(g_viewport_mgr.m_scale);
g_last_scale = g_viewport_mgr.m_scale;
}
if (!first_boot_window.is_open) {
RenderFramebuffer(g_tex, io.DisplaySize.x, io.DisplaySize.y, g_flip_req);
}
ImGui_ImplOpenGL3_NewFrame();
io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad;
ImGui_ImplSDL2_NewFrame(g_sdl_window);
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
g_input_mgr.Update();
ImGui::NewFrame();
ProcessKeyboardShortcuts();
#if defined(DEBUG_NV2A_GL) && defined(CONFIG_RENDERDOC)
if (capture_renderdoc_frame) {
nv2a_dbg_renderdoc_capture_frames(1);
capture_renderdoc_frame = false;
}
#endif
if (g_config.display.ui.show_menubar && !first_boot_window.is_open) {
// Auto-hide main menu after 5s of inactivity
static uint32_t last_check = 0;
float alpha = 1.0;
const uint32_t timeout = 5000;
const float fade_duration = 1000.0;
bool menu_wakeup = g_input_mgr.MouseMoved();
if (menu_wakeup) {
last_check = now;
}
if ((now-last_check) > timeout) {
if (g_config.display.ui.use_animations) {
float t = fmin((float)((now-last_check)-timeout)/fade_duration, 1);
alpha = 1-t;
if (t >= 1) {
alpha = 0;
}
} else {
alpha = 0;
}
}
if (alpha > 0.0) {
ImVec4 tc = ImGui::GetStyle().Colors[ImGuiCol_Text];
tc.w = alpha;
ImGui::PushStyleColor(ImGuiCol_Text, tc);
ImGui::SetNextWindowBgAlpha(alpha);
ShowMainMenu();
ImGui::PopStyleColor();
} else {
g_main_menu_height = 0;
}
}
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow) &&
!g_scene_mgr.IsDisplayingScene()) {
// If the guide button is pressed, wake the ui
bool menu_button = false;
uint32_t buttons = g_input_mgr.CombinedButtons();
if (buttons & CONTROLLER_BUTTON_GUIDE) {
menu_button = true;
}
// Allow controllers without a guide button to also work
if ((buttons & CONTROLLER_BUTTON_BACK) &&
(buttons & CONTROLLER_BUTTON_START)) {
menu_button = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
g_scene_mgr.PushScene(g_main_menu);
} else if (ImGui::IsKeyPressed(ImGuiKey_F2)) {
g_scene_mgr.PushScene(g_popup_menu);
} else if (menu_button ||
(ImGui::IsMouseClicked(ImGuiMouseButton_Right) &&
!ImGui::IsAnyItemFocused() && !ImGui::IsAnyItemHovered())) {
g_scene_mgr.PushScene(g_popup_menu);
}
}
first_boot_window.Draw();
monitor_window.Draw();
apu_window.Draw();
video_window.Draw();
compatibility_reporter_window.Draw();
#if defined(_WIN32)
update_window.Draw();
#endif
g_scene_mgr.Draw();
if (!first_boot_window.is_open) notification_manager.Draw();
// static bool show_demo = true;
// if (show_demo) ImGui::ShowDemoWindow(&show_demo);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (g_vsync != g_config.display.window.vsync) {
g_vsync = g_config.display.window.vsync;
SDL_GL_SetSwapInterval(g_vsync ? 1 : 0);
}
if (g_screenshot_pending) {
SaveScreenshot(g_tex, g_flip_req);
g_screenshot_pending = false;
}
}

192
ui/xui/menubar.cc Normal file
View file

@ -0,0 +1,192 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "common.hh"
#include "main-menu.hh"
#include "menubar.hh"
#include "misc.hh"
#include "widgets.hh"
#include "monitor.hh"
#include "debug.hh"
#include "actions.hh"
#include "compat.hh"
#include "update.hh"
#include "../xemu-os-utils.h"
extern float g_main_menu_height; // FIXME
#ifdef CONFIG_RENDERDOC
static bool capture_renderdoc_frame = false;
#endif
#if defined(__APPLE__)
#define SHORTCUT_MENU_TEXT(c) "Cmd+" #c
#else
#define SHORTCUT_MENU_TEXT(c) "Ctrl+" #c
#endif
void ProcessKeyboardShortcuts(void)
{
if (IsShortcutKeyPressed(ImGuiKey_E)) {
ActionEjectDisc();
}
if (IsShortcutKeyPressed(ImGuiKey_O)) {
ActionLoadDisc();
}
if (IsShortcutKeyPressed(ImGuiKey_P)) {
ActionTogglePause();
}
if (IsShortcutKeyPressed(ImGuiKey_R)) {
ActionReset();
}
if (IsShortcutKeyPressed(ImGuiKey_Q)) {
ActionShutdown();
}
if (ImGui::IsKeyPressed(ImGuiKey_GraveAccent)) {
monitor_window.ToggleOpen();
}
#if defined(DEBUG_NV2A_GL) && defined(CONFIG_RENDERDOC)
if (ImGui::IsKeyPressed(ImGuiKey_F10)) {
nv2a_dbg_renderdoc_capture_frames(1);
}
#endif
}
void ShowMainMenu()
{
bool running = runstate_is_running();
if (ImGui::BeginMainMenuBar())
{
if (ImGui::BeginMenu("Machine"))
{
if (ImGui::MenuItem(running ? "Pause" : "Resume", SHORTCUT_MENU_TEXT(P))) ActionTogglePause();
if (ImGui::MenuItem("Screenshot")) ActionScreenshot();
ImGui::Separator();
if (ImGui::MenuItem("Eject Disc", SHORTCUT_MENU_TEXT(E))) ActionEjectDisc();
if (ImGui::MenuItem("Load Disc...", SHORTCUT_MENU_TEXT(O))) ActionLoadDisc();
ImGui::Separator();
ImGui::MenuItem("Settings", NULL, false, false);
if (ImGui::MenuItem(" General")) g_main_menu.ShowGeneral();
if (ImGui::MenuItem(" Input")) g_main_menu.ShowInput();
if (ImGui::MenuItem(" Display")) g_main_menu.ShowDisplay();
if (ImGui::MenuItem(" Audio")) g_main_menu.ShowAudio();
if (ImGui::MenuItem(" Network")) g_main_menu.ShowNetwork();
if (ImGui::MenuItem(" System")) g_main_menu.ShowSystem();
ImGui::Separator();
if (ImGui::MenuItem("Reset", SHORTCUT_MENU_TEXT(R))) ActionReset();
if (ImGui::MenuItem("Exit", SHORTCUT_MENU_TEXT(Q))) ActionShutdown();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View"))
{
int ui_scale_idx;
if (g_config.display.ui.auto_scale) {
ui_scale_idx = 0;
} else {
ui_scale_idx = g_config.display.ui.scale;
if (ui_scale_idx < 0) ui_scale_idx = 0;
else if (ui_scale_idx > 2) ui_scale_idx = 2;
}
if (ImGui::Combo("UI Scale", &ui_scale_idx,
"Auto\0"
"1x\0"
"2x\0")) {
if (ui_scale_idx == 0) {
g_config.display.ui.auto_scale = true;
} else {
g_config.display.ui.auto_scale = false;
g_config.display.ui.scale = ui_scale_idx;
}
}
int rendering_scale = nv2a_get_surface_scale_factor() - 1;
if (ImGui::Combo("Int. Resolution Scale", &rendering_scale,
"1x\0"
"2x\0"
"3x\0"
"4x\0"
"5x\0"
"6x\0"
"7x\0"
"8x\0"
"9x\0"
"10x\0")) {
nv2a_set_surface_scale_factor(rendering_scale + 1);
}
ImGui::Combo("Display Mode", &g_config.display.ui.fit,
"Center\0Scale\0Scale (Widescreen 16:9)\0Scale "
"(4:3)\0Stretch\0");
ImGui::SameLine();
HelpMarker("Controls how the rendered content should be scaled "
"into the window");
if (ImGui::MenuItem("Fullscreen", SHORTCUT_MENU_TEXT(Alt + F),
xemu_is_fullscreen(), true)) {
xemu_toggle_fullscreen();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Debug"))
{
ImGui::MenuItem("Monitor", "~", &monitor_window.is_open);
ImGui::MenuItem("Audio", NULL, &apu_window.m_is_open);
ImGui::MenuItem("Video", NULL, &video_window.m_is_open);
#if defined(DEBUG_NV2A_GL) && defined(CONFIG_RENDERDOC)
if (nv2a_dbg_renderdoc_available()) {
ImGui::MenuItem("RenderDoc: Capture", NULL, &capture_renderdoc_frame);
}
#endif
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
if (ImGui::MenuItem("Help", NULL)) {
xemu_open_web_browser("https://xemu.app/docs/getting-started/");
}
ImGui::MenuItem("Report Compatibility...", NULL,
&compatibility_reporter_window.is_open);
#if defined(_WIN32)
ImGui::MenuItem("Check for Updates...", NULL, &update_window.is_open);
#endif
ImGui::Separator();
if (ImGui::MenuItem("About")) g_main_menu.ShowAbout();
ImGui::EndMenu();
}
g_main_menu_height = ImGui::GetWindowHeight();
ImGui::EndMainMenuBar();
}
}

22
ui/xui/menubar.hh Normal file
View file

@ -0,0 +1,22 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
void ProcessKeyboardShortcuts(void);
void ShowMainMenu();

24
ui/xui/meson.build Normal file
View file

@ -0,0 +1,24 @@
xemu_ss.add(files(
'actions.cc',
'animation.cc',
'compat.cc',
'debug.cc',
'font-manager.cc',
'gl-helpers.cc',
'input-manager.cc',
'main-menu.cc',
'main.cc',
'menubar.cc',
'monitor.cc',
'notifications.cc',
'popup-menu.cc',
'reporting.cc',
'scene-components.cc',
'scene-manager.cc',
'scene.cc',
'viewport-manager.cc',
'welcome.cc',
'widgets.cc',
))
xemu_ss.add(when: 'CONFIG_WIN32', if_true: files('update.cc'))

106
ui/xui/misc.hh Normal file
View file

@ -0,0 +1,106 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <string>
#include <memory>
#include <stdexcept>
#include <cstdio>
#include "common.hh"
#include "xemu-hud.h"
extern "C" {
#include <noc_file_dialog.h>
}
static inline
bool IsNavInputPressed(ImGuiNavInput i) {
ImGuiIO &io = ImGui::GetIO();
return io.NavInputs[i] > 0.0f && io.NavInputsDownDuration[i] == 0.0f;
}
static inline const char *PausedFileOpen(int flags, const char *filters,
const char *default_path,
const char *default_name)
{
bool is_running = runstate_is_running();
if (is_running) {
vm_stop(RUN_STATE_PAUSED);
}
const char *r = noc_file_dialog_open(flags, filters, default_path, default_name);
if (is_running) {
vm_start();
}
return r;
}
template<typename ... Args>
std::string string_format( const std::string& format, Args ... args )
{
int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0'
if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
auto size = static_cast<size_t>( size_s );
std::unique_ptr<char[]> buf( new char[ size ] );
std::snprintf( buf.get(), size, format.c_str(), args ... );
return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
}
static inline bool IsShortcutKeyPressed(int scancode)
{
ImGuiIO& io = ImGui::GetIO();
const bool is_osx = io.ConfigMacOSXBehaviors;
const bool is_shortcut_key = (is_osx ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl
return is_shortcut_key && ImGui::IsKeyPressed(scancode);
}
static inline float mix(float a, float b, float t)
{
return a*(1.0-t) + (b-a)*t;
}
static inline
int PushWindowTransparencySettings(bool transparent, float alpha_transparent = 0.4, float alpha_opaque = 1.0)
{
float alpha = transparent ? alpha_transparent : alpha_opaque;
ImVec4 c;
c = ImGui::GetStyle().Colors[transparent ? ImGuiCol_WindowBg : ImGuiCol_TitleBg];
c.w *= alpha;
ImGui::PushStyleColor(ImGuiCol_TitleBg, c);
c = ImGui::GetStyle().Colors[transparent ? ImGuiCol_WindowBg : ImGuiCol_TitleBgActive];
c.w *= alpha;
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, c);
c = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
c.w *= alpha;
ImGui::PushStyleColor(ImGuiCol_WindowBg, c);
c = ImGui::GetStyle().Colors[ImGuiCol_Border];
c.w *= alpha;
ImGui::PushStyleColor(ImGuiCol_Border, c);
c = ImGui::GetStyle().Colors[ImGuiCol_FrameBg];
c.w *= alpha;
ImGui::PushStyleColor(ImGuiCol_FrameBg, c);
return 5;
}

155
ui/xui/monitor.cc Normal file
View file

@ -0,0 +1,155 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "monitor.hh"
#include "imgui.h"
#include "misc.hh"
#include "font-manager.hh"
// Portable helpers
static int Stricmp(const char* str1, const char* str2) { int d; while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } return d; }
static char* Strdup(const char *str) { size_t len = strlen(str) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)str, len); }
static void Strtrim(char* str) { char* str_end = str + strlen(str); while (str_end > str && str_end[-1] == ' ') str_end--; *str_end = 0; }
MonitorWindow::MonitorWindow()
{
is_open = false;
memset(InputBuf, 0, sizeof(InputBuf));
HistoryPos = -1;
AutoScroll = true;
ScrollToBottom = false;
}
MonitorWindow::~MonitorWindow()
{
}
void MonitorWindow::Draw()
{
if (!is_open) return;
int style_pop_cnt = PushWindowTransparencySettings(true) + 1;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImU32(ImColor(0, 0, 0, 80)));
ImGuiIO& io = ImGui::GetIO();
ImVec2 window_pos = ImVec2(0,io.DisplaySize.y/2);
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y/2), ImGuiCond_Appearing);
if (ImGui::Begin("Monitor", &is_open, ImGuiWindowFlags_NoCollapse)) {
const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); // 1 separator, 1 input text
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); // Leave room for 1 separator + 1 InputText
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
ImGui::TextUnformatted(xemu_get_monitor_buffer());
ImGui::PopFont();
if (ScrollToBottom || (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
ImGui::SetScrollHereY(1.0f);
}
ScrollToBottom = false;
ImGui::PopStyleVar();
ImGui::EndChild();
ImGui::Separator();
// Command-line
bool reclaim_focus = ImGui::IsWindowAppearing();
ImGui::SetNextItemWidth(-1);
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
if (ImGui::InputText("#commandline", InputBuf, IM_ARRAYSIZE(InputBuf), ImGuiInputTextFlags_EnterReturnsTrue|ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_CallbackHistory, &TextEditCallbackStub, (void*)this)) {
char* s = InputBuf;
Strtrim(s);
if (s[0])
ExecCommand(s);
strcpy(s, "");
reclaim_focus = true;
}
ImGui::PopFont();
// Auto-focus on window apparition
ImGui::SetItemDefaultFocus();
if (reclaim_focus) {
ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget
}
}
ImGui::End();
ImGui::PopStyleColor(style_pop_cnt);
}
void MonitorWindow::ToggleOpen(void)
{
is_open = !is_open;
}
void MonitorWindow::ExecCommand(const char* command_line)
{
xemu_run_monitor_command(command_line);
// Insert into history. First find match and delete it so it can be pushed to the back. This isn't trying to be smart or optimal.
HistoryPos = -1;
for (int i = History.Size-1; i >= 0; i--)
if (Stricmp(History[i], command_line) == 0)
{
free(History[i]);
History.erase(History.begin() + i);
break;
}
History.push_back(Strdup(command_line));
// On commad input, we scroll to bottom even if AutoScroll==false
ScrollToBottom = true;
}
int MonitorWindow::TextEditCallbackStub(ImGuiInputTextCallbackData* data) // In C++11 you are better off using lambdas for this sort of forwarding callbacks
{
MonitorWindow* console = (MonitorWindow*)data->UserData;
return console->TextEditCallback(data);
}
int MonitorWindow::TextEditCallback(ImGuiInputTextCallbackData* data)
{
switch (data->EventFlag)
{
case ImGuiInputTextFlags_CallbackHistory:
{
// Example of HISTORY
const int prev_history_pos = HistoryPos;
if (data->EventKey == ImGuiKey_UpArrow)
{
if (HistoryPos == -1)
HistoryPos = History.Size - 1;
else if (HistoryPos > 0)
HistoryPos--;
}
else if (data->EventKey == ImGuiKey_DownArrow)
{
if (HistoryPos != -1)
if (++HistoryPos >= History.Size)
HistoryPos = -1;
}
// A better implementation would preserve the data on the current input line along with cursor position.
if (prev_history_pos != HistoryPos)
{
const char* history_str = (HistoryPos >= 0) ? History[HistoryPos] : "";
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, history_str);
}
}
}
return 0;
}
MonitorWindow monitor_window;

50
ui/xui/monitor.hh Normal file
View file

@ -0,0 +1,50 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "../xemu-monitor.h"
#include "common.hh"
class MonitorWindow
{
public:
bool is_open;
private:
char InputBuf[256];
ImVector<char*> Items;
ImVector<const char*> Commands;
ImVector<char*> History;
int HistoryPos; // -1: new line, 0..History.Size-1 browsing history.
ImGuiTextFilter Filter;
bool AutoScroll;
bool ScrollToBottom;
public:
MonitorWindow();
~MonitorWindow();
void Draw();
void ToggleOpen(void);
private:
void ExecCommand(const char* command_line);
static int TextEditCallbackStub(ImGuiInputTextCallbackData* data);
int TextEditCallback(ImGuiInputTextCallbackData* data);
};
extern MonitorWindow monitor_window;

155
ui/xui/notifications.cc Normal file
View file

@ -0,0 +1,155 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "notifications.hh"
#include "common.hh"
#include "../xemu-notifications.h"
NotificationManager notification_manager;
NotificationManager::NotificationManager()
{
m_active = false;
}
void NotificationManager::QueueNotification(const char *msg)
{
m_notification_queue.push_back(strdup(msg));
}
void NotificationManager::QueueError(const char *msg)
{
m_error_queue.push_back(strdup(msg));
}
void NotificationManager::Draw()
{
uint32_t now = SDL_GetTicks();
if (m_active) {
// Currently displaying a notification
float t =
(m_notification_end_time - now) / (float)kNotificationDuration;
if (t > 1.0) {
// Notification delivered, free it
free((void *)m_msg);
m_active = false;
} else {
// Notification should be displayed
DrawNotification(t, m_msg);
}
} else {
// Check to see if a notification is pending
if (m_notification_queue.size() > 0) {
m_msg = m_notification_queue[0];
m_active = true;
m_notification_end_time = now + kNotificationDuration;
m_notification_queue.pop_front();
}
}
ImGuiIO& io = ImGui::GetIO();
if (m_error_queue.size() > 0) {
ImGui::OpenPopup("Error");
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x/2, io.DisplaySize.y/2),
ImGuiCond_Always, ImVec2(0.5, 0.5));
}
if (ImGui::BeginPopupModal("Error", NULL, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("%s", m_error_queue[0]);
ImGui::Dummy(ImVec2(0,16));
ImGui::SetItemDefaultFocus();
ImGuiStyle &style = ImGui::GetStyle();
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+2*style.FramePadding.x));
if (ImGui::Button("Ok", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
free((void*)m_error_queue[0]);
m_error_queue.pop_front();
}
ImGui::EndPopup();
}
}
void NotificationManager::DrawNotification(float t, const char *msg)
{
const float DISTANCE = 10.0f;
static int corner = 1;
ImGuiIO& io = ImGui::GetIO();
if (corner != -1)
{
ImVec2 window_pos = ImVec2((corner & 1) ? io.DisplaySize.x - DISTANCE : DISTANCE, (corner & 2) ? io.DisplaySize.y - DISTANCE : DISTANCE);
window_pos.y = g_main_menu_height + DISTANCE;
ImVec2 window_pos_pivot = ImVec2((corner & 1) ? 1.0f : 0.0f, (corner & 2) ? 1.0f : 0.0f);
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot);
}
const float fade_in = 0.1;
const float fade_out = 0.9;
float fade = 0;
if (t < fade_in) {
// Linear fade in
fade = t/fade_in;
} else if (t >= fade_out) {
// Linear fade out
fade = 1-(t-fade_out)/(1-fade_out);
} else {
// Constant
fade = 1.0;
}
ImVec4 color = ImGui::GetStyle().Colors[ImGuiCol_ButtonActive];
color.w *= fade;
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1);
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0,0,0,fade*0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, color);
ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::SetNextWindowBgAlpha(0.90f * fade);
if (ImGui::Begin("Notification", NULL,
ImGuiWindowFlags_Tooltip |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoInputs
))
{
ImGui::Text("%s", msg);
}
ImGui::PopStyleColor();
ImGui::PopStyleColor();
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImGui::End();
}
/* External interface, exposed via xemu-notifications.h */
void xemu_queue_notification(const char *msg)
{
notification_manager.QueueNotification(msg);
}
void xemu_queue_error_message(const char *msg)
{
notification_manager.QueueError(msg);
}

46
ui/xui/notifications.hh Normal file
View file

@ -0,0 +1,46 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <stdint.h>
#include <deque>
#include "../xemu-notifications.h"
class NotificationManager
{
private:
std::deque<const char *> m_notification_queue;
std::deque<const char *> m_error_queue;
const int kNotificationDuration = 4000;
uint32_t m_notification_end_time;
const char *m_msg;
bool m_active;
public:
NotificationManager();
void QueueNotification(const char *msg);
void QueueError(const char *msg);
void Draw();
private:
void DrawNotification(float t, const char *msg);
};
extern NotificationManager notification_manager;

511
ui/xui/popup-menu.cc Normal file
View file

@ -0,0 +1,511 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include <string>
#include <vector>
#include "misc.hh"
#include "actions.hh"
#include "font-manager.hh"
#include "viewport-manager.hh"
#include "scene-manager.hh"
#include "popup-menu.hh"
#include "input-manager.hh"
#include "xemu-hud.h"
#include "IconsFontAwesome6.h"
PopupMenuItemDelegate::~PopupMenuItemDelegate() {}
void PopupMenuItemDelegate::PushMenu(PopupMenu &menu) {}
void PopupMenuItemDelegate::PopMenu() {}
void PopupMenuItemDelegate::ClearMenuStack() {}
void PopupMenuItemDelegate::LostFocus() {}
void PopupMenuItemDelegate::PushFocus() {}
void PopupMenuItemDelegate::PopFocus() {}
bool PopupMenuItemDelegate::DidPop() { return false; }
bool PopupMenuButton(std::string text, std::string icon = "")
{
ImGui::PushFont(g_font_mgr.m_menu_font);
auto button_text = string_format("%s %s", icon.c_str(), text.c_str());
bool status = ImGui::Button(button_text.c_str(), ImVec2(-FLT_MIN, 0));
ImGui::PopFont();
return status;
}
bool PopupMenuCheck(std::string text, std::string icon = "", bool v = false)
{
bool status = PopupMenuButton(text, icon);
if (v) {
ImGui::PushFont(g_font_mgr.m_menu_font);
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const char *icon = ICON_FA_CHECK;
ImVec2 ts_icon = ImGui::CalcTextSize(icon);
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGuiStyle &style = ImGui::GetStyle();
draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x,
p0.y + (p1.y - p0.y - ts_icon.y) / 2),
ImGui::GetColorU32(ImGuiCol_Text), icon);
ImGui::PopFont();
}
return status;
}
bool PopupMenuSubmenuButton(std::string text, std::string icon = "")
{
bool status = PopupMenuButton(text, icon);
ImGui::PushFont(g_font_mgr.m_menu_font);
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const char *right_icon = ICON_FA_CHEVRON_RIGHT;
ImVec2 ts_icon = ImGui::CalcTextSize(right_icon);
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGuiStyle &style = ImGui::GetStyle();
draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x,
p0.y + (p1.y - p0.y - ts_icon.y) / 2),
ImGui::GetColorU32(ImGuiCol_Text), right_icon);
ImGui::PopFont();
return status;
}
bool PopupMenuToggle(std::string text, std::string icon = "", bool *v = nullptr)
{
bool l_v = false;
if (v == NULL) v = &l_v;
ImGuiStyle &style = ImGui::GetStyle();
bool status = PopupMenuButton(text, icon);
ImVec2 p_min = ImGui::GetItemRectMin();
ImVec2 p_max = ImGui::GetItemRectMax();
if (status) *v = !*v;
ImGui::PushFont(g_font_mgr.m_menu_font);
float title_height = ImGui::GetTextLineHeight();
ImGui::PopFont();
float toggle_height = title_height * 0.75;
ImVec2 toggle_size(toggle_height * 1.75, toggle_height);
ImVec2 toggle_pos(p_max.x - toggle_size.x - style.FramePadding.x,
p_min.y + (title_height - toggle_size.y)/2 + style.FramePadding.y);
DrawToggle(*v, ImGui::IsItemHovered(), toggle_pos, toggle_size);
return status;
}
bool PopupMenuSlider(std::string text, std::string icon = "", float *v = NULL)
{
bool status = PopupMenuButton(text, icon);
ImVec2 p_min = ImGui::GetItemRectMin();
ImVec2 p_max = ImGui::GetItemRectMax();
ImGuiStyle &style = ImGui::GetStyle();
float new_v = *v;
if (ImGui::IsItemHovered()) {
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadLStickLeft) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadRStickLeft)) new_v -= 0.05;
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadLStickRight) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadRStickRight)) new_v += 0.05;
}
ImGui::PushFont(g_font_mgr.m_menu_font);
float title_height = ImGui::GetTextLineHeight();
ImGui::PopFont();
float toggle_height = title_height * 0.75;
ImVec2 slider_size(toggle_height * 3.75, toggle_height);
ImVec2 slider_pos(p_max.x - slider_size.x - style.FramePadding.x,
p_min.y + (title_height - slider_size.y)/2 + style.FramePadding.y);
if (ImGui::IsItemActive()) {
ImVec2 mouse = ImGui::GetMousePos();
new_v = GetSliderValueForMousePos(mouse, slider_pos, slider_size);
}
DrawSlider(*v, ImGui::IsItemActive() || ImGui::IsItemHovered(), slider_pos,
slider_size);
*v = fmin(fmax(0, new_v), 1.0);
return status;
}
PopupMenu::PopupMenu() : m_animation(0.12, 0.12), m_ease_direction(0, 0)
{
m_focus = false;
m_pop_focus = false;
}
void PopupMenu::InitFocus()
{
m_pop_focus = true;
}
PopupMenu::~PopupMenu()
{
}
void PopupMenu::Show(const ImVec2 &direction)
{
m_animation.EaseIn();
m_ease_direction = direction;
m_focus = true;
}
void PopupMenu::Hide(const ImVec2 &direction)
{
m_animation.EaseOut();
m_ease_direction = direction;
}
bool PopupMenu::IsAnimating()
{
return m_animation.IsAnimating();
}
void PopupMenu::Draw(PopupMenuItemDelegate &nav)
{
m_animation.Step();
ImGuiIO &io = ImGui::GetIO();
float t = m_animation.GetSinInterpolatedValue();
float window_alpha = t;
ImVec2 window_pos = ImVec2(io.DisplaySize.x / 2 + (1-t) * m_ease_direction.x,
io.DisplaySize.y / 2 + (1-t) * m_ease_direction.y);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, window_alpha);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
g_viewport_mgr.Scale(ImVec2(10, 5)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0.5));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_WindowBg));
ImGui::PushStyleColor(ImGuiCol_NavHighlight, IM_COL32_BLACK_TRANS);
if (m_focus) ImGui::SetNextWindowFocus();
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, ImVec2(0.5, 0.5));
ImGui::SetNextWindowSize(ImVec2(400*g_viewport_mgr.m_scale, 0), ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(0);
ImGui::Begin("###PopupMenu", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings);
if (DrawItems(nav)) nav.PopMenu();
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) nav.LostFocus();
ImVec2 pos = ImGui::GetWindowPos();
ImVec2 sz = ImGui::GetWindowSize();
ImGui::End();
if (!g_input_mgr.IsNavigatingWithController()) {
ImGui::PushFont(g_font_mgr.m_menu_font);
pos.y -= ImGui::GetFrameHeight();
ImGui::SetNextWindowPos(pos);
ImGui::SetNextWindowSize(ImVec2(sz.x, ImGui::GetFrameHeight()));
ImGui::SetNextWindowBgAlpha(0);
ImGui::Begin("###PopupMenuNav", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing);
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 200));
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
if (ImGui::Button(ICON_FA_ARROW_LEFT)) {
nav.PopMenu();
}
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - ImGui::GetStyle().FramePadding.x * 2.0f - ImGui::GetTextLineHeight());
if (ImGui::Button(ICON_FA_XMARK)) {
nav.ClearMenuStack();
}
ImGui::PopStyleColor(2);
ImGui::End();
ImGui::PopFont();
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(7);
m_pop_focus = false;
m_focus = false;
}
bool PopupMenu::DrawItems(PopupMenuItemDelegate &nav)
{
return false;
}
class DisplayModePopupMenu : public virtual PopupMenu {
public:
bool DrawItems(PopupMenuItemDelegate &nav) override
{
const char *values[] = {
"Center", "Scale", "Scale (Widescreen 16:9)", "Scale (4:3)", "Stretch"
};
for (int i = 0; i < CONFIG_DISPLAY_UI_FIT__COUNT; i++) {
bool selected = g_config.display.ui.fit == i;
if (m_focus && selected) ImGui::SetKeyboardFocusHere();
if (PopupMenuCheck(values[i], "", selected))
g_config.display.ui.fit = i;
}
return false;
}
};
extern Scene g_main_menu;
class SettingsPopupMenu : public virtual PopupMenu {
protected:
DisplayModePopupMenu display_mode;
public:
bool DrawItems(PopupMenuItemDelegate &nav) override
{
bool pop = false;
if (m_focus && !m_pop_focus) {
ImGui::SetKeyboardFocusHere();
}
PopupMenuSlider("Volume", ICON_FA_VOLUME_HIGH, &g_config.audio.volume_limit);
bool fs = xemu_is_fullscreen();
if (PopupMenuToggle("Fullscreen", ICON_FA_WINDOW_MAXIMIZE, &fs)) {
xemu_toggle_fullscreen();
}
if (PopupMenuSubmenuButton("Display Mode", ICON_FA_EXPAND)) {
nav.PushFocus();
nav.PushMenu(display_mode);
}
if (PopupMenuButton("All settings...", ICON_FA_SLIDERS)) {
nav.ClearMenuStack();
g_scene_mgr.PushScene(g_main_menu);
}
if (m_pop_focus) {
nav.PopFocus();
}
return pop;
}
};
class RootPopupMenu : public virtual PopupMenu {
protected:
SettingsPopupMenu settings;
bool refocus_first_item;
public:
RootPopupMenu() {
refocus_first_item = false;
}
bool DrawItems(PopupMenuItemDelegate &nav) override
{
bool pop = false;
if (refocus_first_item || (m_focus && !m_pop_focus)) {
ImGui::SetKeyboardFocusHere();
refocus_first_item = false;
}
bool running = runstate_is_running();
if (running) {
if (PopupMenuButton("Pause", ICON_FA_CIRCLE_PAUSE)) {
ActionTogglePause();
refocus_first_item = true;
}
} else {
if (PopupMenuButton("Resume", ICON_FA_CIRCLE_PLAY)) {
ActionTogglePause();
refocus_first_item = true;
}
}
if (PopupMenuButton("Screenshot", ICON_FA_CAMERA)) {
ActionScreenshot();
pop = true;
}
if (PopupMenuButton("Eject Disc", ICON_FA_EJECT)) {
ActionEjectDisc();
pop = true;
}
if (PopupMenuButton("Load Disc...", ICON_FA_COMPACT_DISC)) {
ActionLoadDisc();
pop = true;
}
if (PopupMenuSubmenuButton("Settings", ICON_FA_GEARS)) {
nav.PushFocus();
nav.PushMenu(settings);
}
if (PopupMenuButton("Restart", ICON_FA_ARROWS_ROTATE)) {
ActionReset();
pop = true;
}
if (PopupMenuButton("Exit", ICON_FA_POWER_OFF)) {
ActionShutdown();
pop = true;
}
if (m_pop_focus) {
nav.PopFocus();
}
return pop;
}
};
RootPopupMenu root_menu;
void PopupMenuScene::PushMenu(PopupMenu &menu)
{
menu.Show(m_view_stack.size() ? EASE_VECTOR_LEFT : EASE_VECTOR_DOWN);
m_menus_in_transition.push_back(&menu);
if (m_view_stack.size()) {
auto current = m_view_stack.back();
m_menus_in_transition.push_back(current);
current->Hide(EASE_VECTOR_RIGHT);
}
m_view_stack.push_back(&menu);
}
void PopupMenuScene::PopMenu()
{
if (!m_view_stack.size()) {
return;
}
if (m_view_stack.size() > 1) {
auto previous = m_view_stack[m_view_stack.size() - 2];
previous->Show(EASE_VECTOR_RIGHT);
previous->InitFocus();
m_menus_in_transition.push_back(previous);
}
auto current = m_view_stack.back();
m_view_stack.pop_back();
current->Hide(m_view_stack.size() ? EASE_VECTOR_LEFT : EASE_VECTOR_DOWN);
m_menus_in_transition.push_back(current);
if (!m_view_stack.size()) {
Hide();
}
}
void PopupMenuScene::PushFocus()
{
ImGuiContext *g = ImGui::GetCurrentContext();
m_focus_stack.push_back(std::pair<ImGuiID, ImRect>(g->LastItemData.ID,
g->LastItemData.Rect));
}
void PopupMenuScene::PopFocus()
{
auto next_focus = m_focus_stack.back();
m_focus_stack.pop_back();
ImGuiContext *g = ImGui::GetCurrentContext();
g->NavInitRequest = false;
g->NavInitResultId = next_focus.first;
g->NavInitResultRectRel = ImGui::WindowRectAbsToRel(g->CurrentWindow,
next_focus.second);
// ImGui::NavUpdateAnyRequestFlag();
g->NavAnyRequest = g->NavMoveScoringItems || g->NavInitRequest;// || (IMGUI_DEBUG_NAV_SCORING && g->NavWindow != NULL);
}
void PopupMenuScene::ClearMenuStack()
{
if (m_view_stack.size()) {
auto current = m_view_stack.back();
current->Hide(EASE_VECTOR_DOWN);
m_menus_in_transition.push_back(current);
}
m_view_stack.clear();
m_focus_stack.clear();
Hide();
}
void PopupMenuScene::HandleInput()
{
if (IsNavInputPressed(ImGuiNavInput_Cancel)) {
PopMenu();
}
}
void PopupMenuScene::Show()
{
m_background.Show();
m_nav_control_view.Show();
// m_big_state_icon.Show();
// m_title_info.Show();
if (m_view_stack.size() == 0) {
PushMenu(root_menu);
}
}
void PopupMenuScene::Hide()
{
m_background.Hide();
m_nav_control_view.Hide();
// m_big_state_icon.Hide();
// m_title_info.Hide();
}
bool PopupMenuScene::IsAnimating()
{
return m_menus_in_transition.size() > 0 ||
m_background.IsAnimating() ||
m_nav_control_view.IsAnimating();
// m_big_state_icon.IsAnimating() ||
// m_title_info.IsAnimating();
}
bool PopupMenuScene::Draw()
{
m_background.Draw();
// m_big_state_icon.Draw();
// m_title_info.Draw();
bool displayed = false;
while (m_menus_in_transition.size()) {
auto current = m_menus_in_transition.back();
if (current->IsAnimating()) {
current->Draw(*this);
displayed = true;
break;
}
m_menus_in_transition.pop_back();
}
if (!displayed) {
if (m_view_stack.size()) {
m_view_stack.back()->Draw(*this);
HandleInput();
displayed = true;
}
}
m_nav_control_view.Draw();
return displayed || IsAnimating();
}
void PopupMenuScene::LostFocus()
{
ClearMenuStack();
}
PopupMenuScene g_popup_menu;

85
ui/xui/popup-menu.hh Normal file
View file

@ -0,0 +1,85 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "common.hh"
#include "scene.hh"
#include "scene-components.hh"
#include "animation.hh"
#include "widgets.hh"
class PopupMenu;
class PopupMenuItemDelegate
{
public:
PopupMenuItemDelegate() = default;
virtual ~PopupMenuItemDelegate();
virtual void PushMenu(PopupMenu &menu);
virtual void PopMenu();
virtual void ClearMenuStack();
virtual void LostFocus();
virtual void PushFocus();
virtual void PopFocus();
virtual bool DidPop();
};
class PopupMenu
{
protected:
EasingAnimation m_animation;
ImVec2 m_ease_direction;
bool m_focus;
bool m_pop_focus;
public:
PopupMenu();
void InitFocus();
virtual ~PopupMenu();
void Show(const ImVec2 &direction);
void Hide(const ImVec2 &direction);
bool IsAnimating();
void Draw(PopupMenuItemDelegate &nav);
virtual bool DrawItems(PopupMenuItemDelegate &nav);
};
class PopupMenuScene : virtual public PopupMenuItemDelegate, public Scene {
protected:
std::vector<PopupMenu *> m_view_stack;
std::vector<PopupMenu *> m_menus_in_transition;
std::vector<std::pair<ImGuiID, ImRect>> m_focus_stack;
BackgroundGradient m_background;
NavControlAnnotation m_nav_control_view;
// BigStateIcon m_big_state_icon;
// TitleInfo m_title_info;
public:
void PushMenu(PopupMenu &menu) override;
void PopMenu() override;
void PushFocus() override;
void PopFocus() override;
void ClearMenuStack() override;
void HandleInput();
void Show() override;
void Hide() override;
bool IsAnimating() override;
bool Draw() override;
void LostFocus() override;
};
extern PopupMenuScene g_popup_menu;

View file

@ -1,31 +1,30 @@
/*
* xemu Reporting
*
* Title compatibility and bug report submission.
*
* Copyright (C) 2020-2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//
// xemu Reporting
//
// Title compatibility and bug report submission.
//
// Copyright (C) 2020-2021 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include <glib.h>
#include <glib/gi18n.h>
#include <stdio.h>
#include "xemu-reporting.h"
#include "reporting.hh"
#define CPPHTTPLIB_OPENSSL_SUPPORT 1
#include "httplib.h"
#include "json.hpp"
#include <httplib.h>
#include <json.hpp>
using json = nlohmann::json;
#define DEBUG_COMPAT_SERVICE 0

View file

@ -1,26 +1,23 @@
/*
* xemu Reporting
*
* Title compatibility and bug report submission.
*
* Copyright (C) 2020-2021 Matt Borgerson
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef XEMU_REPORTING_H
#define XEMU_REPORTING_H
//
// xemu Reporting
//
// Title compatibility and bug report submission.
//
// Copyright (C) 2020-2021 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#pragma once
#include <string>
#include <stdint.h>
@ -59,5 +56,3 @@ public:
const std::string &GetSerializedReport();
void SetXbeData(struct xbe *xbe);
};
#endif

278
ui/xui/scene-components.cc Normal file
View file

@ -0,0 +1,278 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "scene-components.hh"
#include "common.hh"
#include "misc.hh"
#include "font-manager.hh"
#include "input-manager.hh"
#include "viewport-manager.hh"
BackgroundGradient::BackgroundGradient()
: m_animation(0.2, 0.2) {}
void BackgroundGradient::Show()
{
m_animation.EaseIn();
}
void BackgroundGradient::Hide()
{
m_animation.EaseOut();
}
bool BackgroundGradient::IsAnimating()
{
return m_animation.IsAnimating();
}
void BackgroundGradient::Draw()
{
m_animation.Step();
float a = m_animation.GetSinInterpolatedValue();
ImU32 top_color = ImGui::GetColorU32(ImVec4(0,0,0,a));
ImU32 bottom_color = ImGui::GetColorU32(ImVec4(0,0,0,fmax(0, fmin(a-0.125, 0.125))));
ImGuiIO &io = ImGui::GetIO();
auto dl = ImGui::GetBackgroundDrawList();
dl->AddRectFilledMultiColor(ImVec2(0, 0), io.DisplaySize, top_color, top_color, bottom_color, bottom_color);
}
NavControlItem::NavControlItem(std::string icon, std::string text)
: m_icon(icon), m_text(text) {}
void NavControlItem::Draw()
{
ImGui::PushFont(g_font_mgr.m_menu_font_small);
auto text = string_format("%s %s", m_icon.c_str(), m_text.c_str());
ImGui::Text("%s", text.c_str());
ImGui::PopFont();
}
NavControlAnnotation::NavControlAnnotation()
: m_animation(0.12,0.12)
{
m_show = false;
m_visible = false;
// FIXME: Based on controller input type, display different icons. Currently
// only showing Xbox scheme
// FIXME: Support configuration of displayed items
m_items.push_back(NavControlItem(ICON_BUTTON_A, "SELECT"));
m_items.push_back(NavControlItem(ICON_BUTTON_B, "BACK"));
}
void NavControlAnnotation::Show()
{
m_show = true;
}
void NavControlAnnotation::Hide()
{
m_show = false;
}
bool NavControlAnnotation::IsAnimating()
{
return m_animation.IsAnimating();
}
void NavControlAnnotation::Draw()
{
if (g_input_mgr.IsNavigatingWithController() && m_show && !m_visible) {
m_animation.EaseIn();
m_visible = true;
} else if ((!g_input_mgr.IsNavigatingWithController() || !m_show) &&
m_visible) {
m_animation.EaseOut();
m_visible = false;
}
m_animation.Step();
ImGuiIO &io = ImGui::GetIO();
ImGui::SetNextWindowBgAlpha(0);
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x - g_viewport_mgr.GetExtents().z,
io.DisplaySize.y - g_viewport_mgr.GetExtents().w),
ImGuiCond_Always, ImVec2(1, 1));
ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
m_animation.GetSinInterpolatedValue());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(30, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0.5));
if (ImGui::Begin("###NavControlAnnotation", NULL,
ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoInputs)) {
int i = 0;
for (auto &button : m_items) {
if (i++) ImGui::SameLine();
button.Draw();
}
}
ImGui::End();
ImGui::PopStyleVar(6);
}
#if 0
class BigStateIcon {
protected:
EasingAnimation m_animation;
public:
BigStateIcon()
: m_animation(0.5, 0.15)
{
}
void Show() {
m_animation.easeIn();
}
void Hide() {
m_animation.easeOut();
}
bool IsAnimating()
{
return m_animation.IsAnimating();
}
void Draw()
{
m_animation.step();
ImGuiIO &io = ImGui::GetIO();
ImGui::SetNextWindowBgAlpha(0);
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - g_viewport_mgr.getExtents().z, g_viewport_mgr.getExtents().y),
ImGuiCond_Always, ImVec2(1, 0));
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_animation.getSinInterpolatedValue());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10*g_viewport_mgr.m_scale, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
if (ImGui::Begin("###BigStateIcon", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) {
ImGui::PushFont(g_font_mgr.m_bigStateIconFont);
ImGui::Text("%s", ICON_FA_PAUSE);
ImGui::PopFont();
}
ImGui::End();
ImGui::PopStyleVar(4);
}
};
class TitleInfo
{
protected:
GLuint screenshot;
ImVec2 size;
EasingAnimation m_animation;
public:
TitleInfo()
: m_animation(0.2, 0.2)
{
screenshot = 0;
}
void Show()
{
m_animation.easeIn();
}
void Hide()
{
m_animation.easeOut();
}
bool IsAnimating()
{
return m_animation.IsAnimating();
}
void initScreenshot()
{
if (screenshot == 0) {
glGenTextures(1, &screenshot);
int w, h, n;
stbi_set_flip_vertically_on_load(0);
unsigned char *data = stbi_load("./data/cover_front.jpg", &w, &h, &n, 4);
assert(data);
assert(n == 4 || n == 3);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, screenshot);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
// Fix width
float width = 100;
float height = width*h/w;
size = ImVec2(width, height);
}
}
void Draw()
{
initScreenshot();
m_animation.step();
ImGui::SetNextWindowSize(g_viewport_mgr.scale(ImVec2(600, 600)));
ImGui::SetNextWindowBgAlpha(0);
ImGui::SetNextWindowPos(ImVec2(g_viewport_mgr.getExtents().x,
g_viewport_mgr.getExtents().y),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_animation.getSinInterpolatedValue());
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0.5));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, g_viewport_mgr.m_scale*6);
if (ImGui::Begin("###TitleInfo", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) {
ImGui::Columns(2, NULL, false);
ImGuiStyle &style = ImGui::GetStyle();
ImVec2 scaled_size = g_viewport_mgr.scale(size);
ImGui::SetColumnWidth(0, scaled_size.x + style.ItemSpacing.x);
ImGui::Dummy(scaled_size);
ImVec2 p0 = ImGui::GetItemRectMin();
ImVec2 p1 = ImGui::GetItemRectMax();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
draw_list->AddImageRounded((ImTextureID)screenshot, p0, p1, ImVec2(0, 0), ImVec2(1, 1), ImGui::GetColorU32(ImVec4(1,1,1,m_animation.getSinInterpolatedValue())), 3*g_viewport_mgr.m_scale);
ImGui::NextColumn();
ImGui::PushFont(g_font_mgr.m_menuFont);
ImGui::Text("Halo: Combat Evolved");
ImGui::PopFont();
ImGui::PushFont(g_font_mgr.m_menuFontSmall);
ImGui::Text("NTSC MS-004");
ImGui::PopFont();
ImGui::Columns(1);
}
ImGui::End();
ImGui::PopStyleVar(6);
}
};
#endif

View file

@ -0,0 +1,91 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <string>
#include <vector>
#include "animation.hh"
class BackgroundGradient
{
protected:
EasingAnimation m_animation;
public:
BackgroundGradient();
void Show();
void Hide();
bool IsAnimating();
void Draw();
};
class NavControlItem
{
protected:
std::string m_icon;
std::string m_text;
public:
NavControlItem(std::string icon, std::string text);
void Draw();
};
class NavControlAnnotation
{
protected:
EasingAnimation m_animation;
std::vector<NavControlItem> m_items;
bool m_show, m_visible;
public:
NavControlAnnotation();
void Show();
void Hide();
bool IsAnimating();
void Draw();
};
#if 0
class BigStateIcon {
protected:
EasingAnimation m_animation;
public:
BigStateIcon();
void Show();
void Hide();
bool IsAnimating();
void Draw();
};
class TitleInfo
{
protected:
GLuint screenshot;
ImVec2 size;
EasingAnimation m_animation;
public:
TitleInfo();
void Show();
void Hide();
bool IsAnimating();
void initScreenshot();
void Draw();
};
#endif

52
ui/xui/scene-manager.cc Normal file
View file

@ -0,0 +1,52 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "scene-manager.hh"
SceneManager g_scene_mgr;
SceneManager::SceneManager()
{
m_active_scene = nullptr;
}
void SceneManager::PushScene(Scene &scene)
{
m_scenes.insert(m_scenes.begin(), &scene);
}
bool SceneManager::IsDisplayingScene()
{
return m_active_scene != nullptr || m_scenes.size() > 0;
}
bool SceneManager::Draw()
{
if (m_active_scene) {
bool finished = !m_active_scene->Draw();
if (finished) {
m_active_scene = nullptr;
}
return true;
} else if (m_scenes.size()) {
m_active_scene = m_scenes.back();
m_scenes.pop_back();
m_active_scene->Show();
}
return false;
}

36
ui/xui/scene-manager.hh Normal file
View file

@ -0,0 +1,36 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include <vector>
#include "scene.hh"
class SceneManager
{
protected:
Scene *m_active_scene;
std::vector<Scene *> m_scenes;
public:
SceneManager();
void PushScene(Scene &scene);
bool IsDisplayingScene();
bool Draw();
};
extern SceneManager g_scene_mgr;

25
ui/xui/scene.cc Normal file
View file

@ -0,0 +1,25 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "scene.hh"
Scene::~Scene() {}
void Scene::Show() {}
void Scene::Hide() {}
bool Scene::IsAnimating() { return false; }
bool Scene::Draw() { return false; }

30
ui/xui/scene.hh Normal file
View file

@ -0,0 +1,30 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
class Scene
{
public:
Scene() = default;
virtual ~Scene();
virtual void Show();
virtual void Hide();
virtual bool IsAnimating();
virtual bool Draw();
};

276
ui/xui/update.cc Normal file
View file

@ -0,0 +1,276 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "common.hh"
#include "update.hh"
#include "viewport-manager.hh"
#include <stdio.h>
#include <stdlib.h>
#include <SDL_filesystem.h>
#include "util/miniz/miniz.h"
#include "xemu-version.h"
#if defined(_WIN32)
const char *version_host = "raw.githubusercontent.com";
const char *version_uri = "/mborgerson/xemu/ppa-snapshot/XEMU_VERSION";
const char *download_host = "github.com";
const char *download_uri = "/mborgerson/xemu/releases/latest/download/xemu-win-release.zip";
#else
FIXME
#endif
#define CPPHTTPLIB_OPENSSL_SUPPORT 1
#include <httplib.h>
#define DPRINTF(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__);
AutoUpdateWindow update_window;
AutoUpdateWindow::AutoUpdateWindow()
{
is_open = false;
}
void AutoUpdateWindow::CheckForUpdates()
{
updater.check_for_update([this](){
is_open |= updater.is_update_available();
});
}
void AutoUpdateWindow::Draw()
{
if (!is_open) return;
ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_viewport_mgr.m_scale, 0.0f));
if (!ImGui::Begin("Update", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::End();
return;
}
if (ImGui::IsWindowAppearing() && !updater.is_update_available()) {
updater.check_for_update();
}
const char *status_msg[] = {
"",
"An error has occured. Try again.",
"Checking for update...",
"Downloading update...",
"Update successful! Restart to launch updated version of xemu."
};
const char *available_msg[] = {
"Update availability unknown.",
"This version of xemu is up to date.",
"An updated version of xemu is available!",
};
if (updater.get_status() == UPDATER_IDLE) {
ImGui::Text(available_msg[updater.get_update_availability()]);
} else {
ImGui::Text(status_msg[updater.get_status()]);
}
if (updater.is_updating()) {
ImGui::ProgressBar(updater.get_update_progress_percentage()/100.0f,
ImVec2(-1.0f, 0.0f));
}
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
ImGui::Separator();
ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
float w = (130)*g_viewport_mgr.m_scale;
float bw = w + (10)*g_viewport_mgr.m_scale;
ImGui::SetCursorPosX(ImGui::GetWindowWidth()-bw);
if (updater.is_checking_for_update() || updater.is_updating()) {
if (ImGui::Button("Cancel", ImVec2(w, 0))) {
updater.cancel();
}
} else {
if (updater.is_pending_restart()) {
if (ImGui::Button("Restart", ImVec2(w, 0))) {
updater.restart_to_updated();
}
} else if (updater.is_update_available()) {
if (ImGui::Button("Update", ImVec2(w, 0))) {
updater.update();
}
} else {
if (ImGui::Button("Check for Update", ImVec2(w, 0))) {
updater.check_for_update();
}
}
}
ImGui::End();
}
Updater::Updater()
{
m_status = UPDATER_IDLE;
m_update_availability = UPDATE_AVAILABILITY_UNKNOWN;
m_update_percentage = 0;
m_latest_version = "Unknown";
m_should_cancel = false;
}
void Updater::check_for_update(UpdaterCallback on_complete)
{
if (m_status == UPDATER_IDLE || m_status == UPDATER_ERROR) {
m_on_complete = on_complete;
qemu_thread_create(&m_thread, "update_worker",
&Updater::checker_thread_worker_func,
this, QEMU_THREAD_JOINABLE);
}
}
void *Updater::checker_thread_worker_func(void *updater)
{
((Updater *)updater)->check_for_update_internal();
return NULL;
}
void Updater::check_for_update_internal()
{
httplib::SSLClient cli(version_host, 443);
cli.set_follow_location(true);
cli.set_timeout_sec(5);
auto res = cli.Get(version_uri, [this](uint64_t len, uint64_t total) {
m_update_percentage = len*100/total;
return !m_should_cancel;
});
if (m_should_cancel) {
m_should_cancel = false;
m_status = UPDATER_IDLE;
goto finished;
} else if (!res || res->status != 200) {
m_status = UPDATER_ERROR;
goto finished;
}
if (strcmp(xemu_version, res->body.c_str())) {
m_update_availability = UPDATE_AVAILABLE;
} else {
m_update_availability = UPDATE_NOT_AVAILABLE;
}
m_latest_version = res->body;
m_status = UPDATER_IDLE;
finished:
if (m_on_complete) {
m_on_complete();
}
}
void Updater::update()
{
if (m_status == UPDATER_IDLE || m_status == UPDATER_ERROR) {
m_status = UPDATER_UPDATING;
qemu_thread_create(&m_thread, "update_worker",
&Updater::update_thread_worker_func,
this, QEMU_THREAD_JOINABLE);
}
}
void *Updater::update_thread_worker_func(void *updater)
{
((Updater *)updater)->update_internal();
return NULL;
}
void Updater::update_internal()
{
httplib::SSLClient cli(download_host, 443);
cli.set_follow_location(true);
cli.set_timeout_sec(5);
auto res = cli.Get(download_uri, [this](uint64_t len, uint64_t total) {
m_update_percentage = len*100/total;
return !m_should_cancel;
});
if (m_should_cancel) {
m_should_cancel = false;
m_status = UPDATER_IDLE;
return;
} else if (!res || res->status != 200) {
m_status = UPDATER_ERROR;
return;
}
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
if (!mz_zip_reader_init_mem(&zip, res->body.data(), res->body.size(), 0)) {
DPRINTF("mz_zip_reader_init_mem failed\n");
m_status = UPDATER_ERROR;
return;
}
mz_uint num_files = mz_zip_reader_get_num_files(&zip);
for (mz_uint file_idx = 0; file_idx < num_files; file_idx++) {
mz_zip_archive_file_stat fstat;
if (!mz_zip_reader_file_stat(&zip, file_idx, &fstat)) {
DPRINTF("mz_zip_reader_file_stat failed for file #%d\n", file_idx);
goto errored;
}
if (fstat.m_filename[strlen(fstat.m_filename)-1] == '/') {
/* FIXME: mkdirs */
DPRINTF("FIXME: subdirs not handled yet\n");
goto errored;
}
char *dst_path = g_strdup_printf("%s%s", SDL_GetBasePath(), fstat.m_filename);
DPRINTF("extracting %s to %s\n", fstat.m_filename, dst_path);
if (!strcmp(fstat.m_filename, "xemu.exe")) {
// We cannot overwrite current executable, but we can move it
char *renamed_path = g_strdup_printf("%s%s", SDL_GetBasePath(), "xemu-previous.exe");
MoveFileExA(dst_path, renamed_path, MOVEFILE_REPLACE_EXISTING);
g_free(renamed_path);
}
if (!mz_zip_reader_extract_to_file(&zip, file_idx, dst_path, 0)) {
DPRINTF("mz_zip_reader_extract_to_file failed to create %s\n", dst_path);
g_free(dst_path);
goto errored;
}
g_free(dst_path);
}
m_status = UPDATER_UPDATE_SUCCESSFUL;
goto cleanup_zip;
errored:
m_status = UPDATER_ERROR;
cleanup_zip:
mz_zip_reader_end(&zip);
}
extern "C" {
extern char **gArgv;
}
void Updater::restart_to_updated()
{
char *target_exec = g_strdup_printf("%s%s", SDL_GetBasePath(), "xemu.exe");
DPRINTF("Restarting to updated executable %s\n", target_exec);
_execv(target_exec, gArgv);
DPRINTF("Launching updated executable failed\n");
exit(1);
}

92
ui/xui/update.hh Normal file
View file

@ -0,0 +1,92 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#if defined(_WIN32)
#include <string>
#include <stdint.h>
#include <functional>
extern "C" {
#include "qemu/osdep.h"
#include "qemu-common.h"
#include "qemu/thread.h"
}
typedef enum {
UPDATE_AVAILABILITY_UNKNOWN,
UPDATE_NOT_AVAILABLE,
UPDATE_AVAILABLE
} UpdateAvailability;
typedef enum {
UPDATER_IDLE,
UPDATER_ERROR,
UPDATER_CHECKING_FOR_UPDATE,
UPDATER_UPDATING,
UPDATER_UPDATE_SUCCESSFUL
} UpdateStatus;
using UpdaterCallback = std::function<void(void)>;
class Updater {
private:
UpdateAvailability m_update_availability;
int m_update_percentage;
QemuThread m_thread;
std::string m_latest_version;
bool m_should_cancel;
UpdateStatus m_status;
UpdaterCallback m_on_complete;
public:
Updater();
UpdateStatus get_status() { return m_status; }
UpdateAvailability get_update_availability() { return m_update_availability; }
bool is_errored() { return m_status == UPDATER_ERROR; }
bool is_pending_restart() { return m_status == UPDATER_UPDATE_SUCCESSFUL; }
bool is_update_available() { return m_update_availability == UPDATE_AVAILABLE; }
bool is_checking_for_update() { return m_status == UPDATER_CHECKING_FOR_UPDATE; }
bool is_updating() { return m_status == UPDATER_UPDATING; }
std::string get_update_version() { return m_latest_version; }
void cancel() { m_should_cancel = true; }
void update();
void update_internal();
void check_for_update(UpdaterCallback on_complete = nullptr);
void check_for_update_internal();
int get_update_progress_percentage() { return m_update_percentage; }
static void *update_thread_worker_func(void *updater);
static void *checker_thread_worker_func(void *updater);
void restart_to_updated(void);
};
class AutoUpdateWindow
{
protected:
Updater updater;
public:
bool is_open;
AutoUpdateWindow();
void CheckForUpdates();
void Draw();
};
extern AutoUpdateWindow update_window;
#endif //_ WIN32

View file

@ -0,0 +1,89 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "viewport-manager.hh"
ViewportManager g_viewport_mgr;
ViewportManager::ViewportManager() {
m_scale = 1;
m_extents.x = 25 * m_scale; // Distance from Left
m_extents.y = 25 * m_scale; // '' Top
m_extents.z = 25 * m_scale; // '' Right
m_extents.w = 25 * m_scale; // '' Bottom
}
ImVec4 ViewportManager::GetExtents()
{
return m_extents;
}
#if 0
void ViewportManager::DrawExtents()
{
ImGuiIO &io = ImGui::GetIO();
ImVec2 tl(m_extents.x, m_extents.y);
ImVec2 tr(io.DisplaySize.x - m_extents.z, m_extents.y);
ImVec2 br(io.DisplaySize.x - m_extents.z, io.DisplaySize.y - m_extents.w);
ImVec2 bl(m_extents.x, io.DisplaySize.y - m_extents.w);
auto dl = ImGui::GetForegroundDrawList();
ImU32 color = 0xffff00ff;
dl->AddLine(tl, tr, color, 2.0);
dl->AddLine(tr, br, color, 2.0);
dl->AddLine(br, bl, color, 2.0);
dl->AddLine(bl, tl, color, 2.0);
dl->AddLine(tl, br, color, 2.0);
dl->AddLine(bl, tr, color, 2.0);
}
#endif
ImVec2 ViewportManager::Scale(const ImVec2 vec2)
{
return ImVec2(vec2.x * m_scale, vec2.y * m_scale);
}
void ViewportManager::Update()
{
ImGuiIO &io = ImGui::GetIO();
if (g_config.display.ui.auto_scale) {
if (io.DisplaySize.x > 1920) {
g_config.display.ui.scale = 2;
} else {
g_config.display.ui.scale = 1;
}
}
m_scale = g_config.display.ui.scale;
if (m_scale < 1) {
m_scale = 1;
} else if (m_scale > 2) {
m_scale = 2;
}
if (io.DisplaySize.x > 640*m_scale) {
m_extents.x = 25 * m_scale; // Distance from Left
m_extents.y = 25 * m_scale; // '' Top
m_extents.z = 25 * m_scale; // '' Right
m_extents.w = 25 * m_scale; // '' Bottom
} else {
m_extents = ImVec4(0,0,0,0);
}
}

View file

@ -0,0 +1,36 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "common.hh"
class ViewportManager
{
protected:
ImVec4 m_extents;
public:
float m_scale;
ViewportManager();
ImVec4 GetExtents();
void DrawExtents();
ImVec2 Scale(const ImVec2 vec2);
void Update();
};
extern ViewportManager g_viewport_mgr;

99
ui/xui/welcome.cc Normal file
View file

@ -0,0 +1,99 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "ui/xui/viewport-manager.hh"
#include "common.hh"
#include "imgui.h"
#include "viewport-manager.hh"
#include "welcome.hh"
#include "widgets.hh"
#include "misc.hh"
#include "gl-helpers.hh"
#include "xemu-version.h"
#include "main-menu.hh"
FirstBootWindow::FirstBootWindow()
{
is_open = false;
}
void FirstBootWindow::Draw()
{
if (!is_open) return;
ImVec2 size(400*g_viewport_mgr.m_scale, 300*g_viewport_mgr.m_scale);
ImGuiIO& io = ImGui::GetIO();
ImVec2 window_pos = ImVec2((io.DisplaySize.x - size.x)/2, (io.DisplaySize.y - size.y)/2);
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always);
ImGui::SetNextWindowSize(size, ImGuiCond_Appearing);
if (!ImGui::Begin("First Boot", &is_open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDecoration)) {
ImGui::End();
return;
}
static uint32_t time_start = 0;
if (ImGui::IsWindowAppearing()) {
time_start = SDL_GetTicks();
}
uint32_t now = SDL_GetTicks() - time_start;
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50*g_viewport_mgr.m_scale);
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256*g_viewport_mgr.m_scale)/2);
logo_fbo->Target();
ImTextureID id = (ImTextureID)(intptr_t)logo_fbo->Texture();
float t_w = 256.0;
float t_h = 256.0;
float x_off = 0;
ImGui::Image(id,
ImVec2((t_w-x_off)*g_viewport_mgr.m_scale, t_h*g_viewport_mgr.m_scale),
ImVec2(x_off/t_w, t_h/t_h),
ImVec2(t_w/t_w, 0));
if (ImGui::IsItemClicked()) {
time_start = SDL_GetTicks();
}
RenderLogo(now, 0x42e335ff, 0x42e335ff, 0x00000000);
logo_fbo->Restore();
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-100*g_viewport_mgr.m_scale);
ImGui::SetCursorPosX(10*g_viewport_mgr.m_scale);
ImGui::Dummy(ImVec2(0,20*g_viewport_mgr.m_scale));
const char *msg = "Configure machine settings to get started";
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
ImGui::Text("%s", msg);
ImGui::Dummy(ImVec2(0,20*g_viewport_mgr.m_scale));
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120*g_viewport_mgr.m_scale)/2);
ImGui::SetItemDefaultFocus();
if (ImGui::Button("Settings", ImVec2(120*g_viewport_mgr.m_scale, 0))) {
g_main_menu.ShowSystem();
g_config.general.show_welcome = false;
}
ImGui::Dummy(ImVec2(0,20*g_viewport_mgr.m_scale));
msg = "Visit https://xemu.app for more information";
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
Hyperlink(msg, "https://xemu.app");
ImGui::End();
}
FirstBootWindow first_boot_window;

29
ui/xui/welcome.hh Normal file
View file

@ -0,0 +1,29 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
class FirstBootWindow
{
public:
bool is_open;
FirstBootWindow();
void Draw();
};
extern FirstBootWindow first_boot_window;

506
ui/xui/widgets.cc Normal file
View file

@ -0,0 +1,506 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "widgets.hh"
#include "misc.hh"
#include "font-manager.hh"
#include "viewport-manager.hh"
#include "ui/xemu-os-utils.h"
void Separator()
{
// XXX: IDK. Maybe there's a better way to draw a separator ( ImGui::Separator() ) that cuts through window
// padding... Just grab the draw list and draw the line with outer clip rect
float thickness = 1 * g_viewport_mgr.m_scale;
ImGuiWindow *window = ImGui::GetCurrentWindow();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImRect window_rect = window->Rect();
ImVec2 size = ImVec2(window_rect.GetWidth(), thickness);
ImVec2 p0(window_rect.Min.x, ImGui::GetCursorScreenPos().y);
ImVec2 p1(p0.x + size.x, p0.y);
ImGui::PushClipRect(window_rect.Min, window_rect.Max, false);
draw_list->AddLine(p0, p1, ImGui::GetColorU32(ImGuiCol_Separator), thickness);
ImGui::PopClipRect();
ImGui::Dummy(size);
}
void SectionTitle(const char *title)
{
ImGui::Spacing();
ImGui::PushFont(g_font_mgr.m_menu_font_medium);
ImGui::Text("%s", title);
ImGui::PopFont();
Separator();
}
float GetWidgetTitleDescriptionHeight(const char *title,
const char *description)
{
ImGui::PushFont(g_font_mgr.m_menu_font_medium);
float h = ImGui::GetFrameHeight();
ImGui::PopFont();
if (description) {
ImGuiStyle &style = ImGui::GetStyle();
h += style.ItemInnerSpacing.y;
ImGui::PushFont(g_font_mgr.m_default_font);
h += ImGui::GetTextLineHeight();
ImGui::PopFont();
}
return h;
}
void WidgetTitleDescription(const char *title, const char *description,
ImVec2 pos)
{
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGuiStyle &style = ImGui::GetStyle();
ImVec2 text_pos = pos;
text_pos.x += style.FramePadding.x;
text_pos.y += style.FramePadding.y;
ImGui::PushFont(g_font_mgr.m_menu_font_medium);
float title_height = ImGui::GetTextLineHeight();
draw_list->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), title);
ImGui::PopFont();
if (description) {
text_pos.y += title_height + style.ItemInnerSpacing.y;
ImGui::PushFont(g_font_mgr.m_default_font);
draw_list->AddText(text_pos, ImGui::GetColorU32(ImVec4(0.94f, 0.94f, 0.94f, 0.70f)), description);
ImGui::PopFont();
}
}
void WidgetTitleDescriptionItem(const char *str_id, const char *description)
{
ImVec2 p = ImGui::GetCursorScreenPos();
ImVec2 size(ImGui::GetColumnWidth(),
GetWidgetTitleDescriptionHeight(str_id, description));
WidgetTitleDescription(str_id, description, p);
// XXX: Internal API
ImRect bb(p, ImVec2(p.x + size.x, p.y + size.y));
ImGui::ItemSize(size, 0.0f);
ImGui::ItemAdd(bb, 0);
}
float GetSliderRadius(ImVec2 size)
{
return size.y * 0.5;
}
float GetSliderTrackXOffset(ImVec2 size)
{
return GetSliderRadius(size);
}
float GetSliderTrackWidth(ImVec2 size)
{
return size.x - GetSliderRadius(size) * 2;
}
float GetSliderValueForMousePos(ImVec2 mouse, ImVec2 pos, ImVec2 size)
{
return (mouse.x - pos.x - GetSliderTrackXOffset(size)) /
GetSliderTrackWidth(size);
}
void DrawSlider(float v, bool hovered, ImVec2 pos, ImVec2 size)
{
ImDrawList *draw_list = ImGui::GetWindowDrawList();
float radius = GetSliderRadius(size);
float rounding = size.y * 0.25;
float slot_half_height = size.y * 0.125;
const bool circular_grab = false;
ImU32 bg = hovered ? ImGui::GetColorU32(ImGuiCol_FrameBgActive)
: ImGui::GetColorU32(ImGuiCol_CheckMark);
ImVec2 pmid(pos.x + radius + v*(size.x - radius*2), pos.y + size.y / 2);
ImVec2 smin(pos.x + rounding, pmid.y - slot_half_height);
ImVec2 smax(pmid.x, pmid.y + slot_half_height);
draw_list->AddRectFilled(smin, smax, bg, rounding);
bg = hovered ? ImGui::GetColorU32(ImGuiCol_FrameBgHovered)
: ImGui::GetColorU32(ImGuiCol_FrameBg);
smin.x = pmid.x;
smax.x = pos.x + size.x - rounding;
draw_list->AddRectFilled(smin, smax, bg, rounding);
if (circular_grab) {
draw_list->AddCircleFilled(pmid, radius * 0.8, ImGui::GetColorU32(ImGuiCol_SliderGrab));
} else {
ImVec2 offs(radius*0.8, radius*0.8);
draw_list->AddRectFilled(pmid - offs, pmid + offs, ImGui::GetColorU32(ImGuiCol_SliderGrab), rounding);
}
}
void DrawToggle(bool enabled, bool hovered, ImVec2 pos, ImVec2 size)
{
ImDrawList *draw_list = ImGui::GetWindowDrawList();
float radius = size.y * 0.5;
float rounding = size.y * 0.25;
float slot_half_height = size.y * 0.5;
const bool circular_grab = false;
ImU32 bg = hovered ? ImGui::GetColorU32(enabled ? ImGuiCol_FrameBgActive : ImGuiCol_FrameBgHovered)
: ImGui::GetColorU32(enabled ? ImGuiCol_CheckMark : ImGuiCol_FrameBg);
ImVec2 pmid(pos.x + radius + (int)enabled * (size.x - radius * 2), pos.y + size.y / 2);
ImVec2 smin(pos.x, pmid.y - slot_half_height);
ImVec2 smax(pos.x + size.x, pmid.y + slot_half_height);
draw_list->AddRectFilled(smin, smax, bg, rounding);
if (circular_grab) {
draw_list->AddCircleFilled(pmid, radius * 0.8, ImGui::GetColorU32(ImGuiCol_SliderGrab));
} else {
ImVec2 offs(radius*0.8, radius*0.8);
draw_list->AddRectFilled(pmid - offs, pmid + offs, ImGui::GetColorU32(ImGuiCol_SliderGrab), rounding);
}
}
bool Toggle(const char *str_id, bool *v, const char *description)
{
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
ImGuiStyle &style = ImGui::GetStyle();
ImGui::PushFont(g_font_mgr.m_menu_font_medium);
float title_height = ImGui::GetTextLineHeight();
ImGui::PopFont();
ImVec2 p = ImGui::GetCursorScreenPos();
ImVec2 bb(ImGui::GetColumnWidth(),
GetWidgetTitleDescriptionHeight(str_id, description));
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0));
ImGui::PushID(str_id);
bool status = ImGui::Button("###toggle_button", bb);
if (status) {
*v = !*v;
}
ImGui::PopID();
ImGui::PopStyleVar();
const ImVec2 p_min = ImGui::GetItemRectMin();
const ImVec2 p_max = ImGui::GetItemRectMax();
WidgetTitleDescription(str_id, description, p);
float toggle_height = title_height * 0.9;
ImVec2 toggle_size(toggle_height * 1.75, toggle_height);
ImVec2 toggle_pos(p_max.x - toggle_size.x - style.FramePadding.x,
p_min.y + (title_height - toggle_size.y)/2 + style.FramePadding.y);
DrawToggle(*v, ImGui::IsItemHovered(), toggle_pos, toggle_size);
ImGui::PopStyleColor();
return status;
}
void Slider(const char *str_id, float *v, const char *description)
{
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
ImGuiStyle &style = ImGui::GetStyle();
ImGuiWindow *window = ImGui::GetCurrentWindow();
ImGui::PushFont(g_font_mgr.m_menu_font_medium);
float title_height = ImGui::GetTextLineHeight();
ImGui::PopFont();
ImVec2 p = ImGui::GetCursorScreenPos();
ImVec2 size(ImGui::GetColumnWidth(),
GetWidgetTitleDescriptionHeight(str_id, description));
WidgetTitleDescription(str_id, description, p);
// XXX: Internal API
ImVec2 wpos = ImGui::GetCursorPos();
ImRect bb(p, ImVec2(p.x + size.x, p.y + size.y));
ImGui::ItemSize(size, 0.0f);
ImGui::ItemAdd(bb, 0);
ImGui::SetItemAllowOverlap();
ImGui::SameLine(0, 0);
ImVec2 slider_size(size.x * 0.4, title_height * 0.9);
ImVec2 slider_pos(bb.Max.x - slider_size.x - style.FramePadding.x,
p.y + (title_height - slider_size.y)/2 + style.FramePadding.y);
ImGui::SetCursorPos(ImVec2(wpos.x + size.x - slider_size.x - style.FramePadding.x,
wpos.y));
ImGui::InvisibleButton("###slider", slider_size, 0);
if (ImGui::IsItemHovered()) {
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadLStickLeft) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadRStickLeft)) {
*v -= 0.05;
}
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadLStickRight) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadRStickRight)) {
*v += 0.05;
}
if (
ImGui::IsKeyDown(ImGuiKey_LeftArrow) ||
ImGui::IsKeyDown(ImGuiKey_GamepadDpadLeft) ||
ImGui::IsKeyDown(ImGuiKey_GamepadLStickLeft) ||
ImGui::IsKeyDown(ImGuiKey_GamepadRStickLeft) ||
ImGui::IsKeyDown(ImGuiKey_RightArrow) ||
ImGui::IsKeyDown(ImGuiKey_GamepadDpadRight) ||
ImGui::IsKeyDown(ImGuiKey_GamepadLStickRight) ||
ImGui::IsKeyDown(ImGuiKey_GamepadRStickRight)
) {
ImGui::NavMoveRequestCancel();
}
}
if (ImGui::IsItemActive()) {
ImVec2 mouse = ImGui::GetMousePos();
*v = GetSliderValueForMousePos(mouse, slider_pos, slider_size);
}
*v = fmax(0, fmin(*v, 1));
DrawSlider(*v, ImGui::IsItemHovered() || ImGui::IsItemActive(), slider_pos,
slider_size);
ImVec2 slider_max = ImVec2(slider_pos.x + slider_size.x, slider_pos.y + slider_size.y);
ImGui::RenderNavHighlight(ImRect(slider_pos, slider_max), window->GetID("###slider"));
ImGui::PopStyleColor();
}
bool FilePicker(const char *str_id, const char **buf, const char *filters,
bool dir)
{
bool changed = false;
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
ImGuiStyle &style = ImGui::GetStyle();
ImVec2 p = ImGui::GetCursorScreenPos();
const char *desc = strlen(*buf) ? *buf : "(None Selected)";
ImVec2 bb(ImGui::GetColumnWidth(),
GetWidgetTitleDescriptionHeight(str_id, desc));
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0));
ImGui::PushID(str_id);
bool status = ImGui::Button("###file_button", bb);
if (status) {
const char *new_path =
PausedFileOpen(dir ? NOC_FILE_DIALOG_DIR : NOC_FILE_DIALOG_OPEN,
filters, *buf, NULL);
if (new_path) {
free((void*)*buf);
*buf = strdup(new_path);
changed = true;
}
}
ImGui::PopID();
ImGui::PopStyleVar();
WidgetTitleDescription(str_id, desc, p);
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGui::PushFont(g_font_mgr.m_menu_font);
const char *icon = dir ? ICON_FA_FOLDER : ICON_FA_FILE;
ImVec2 ts_icon = ImGui::CalcTextSize(icon);
draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x,
p0.y + (p1.y - p0.y - ts_icon.y) / 2),
ImGui::GetColorU32(ImGuiCol_Text), icon);
ImGui::PopFont();
ImGui::PopStyleColor();
return changed;
}
void DrawComboChevron()
{
ImGui::PushFont(g_font_mgr.m_menu_font);
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const char *icon = ICON_FA_CHEVRON_DOWN;
ImVec2 ts_icon = ImGui::CalcTextSize(icon);
ImGuiStyle &style = ImGui::GetStyle();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x,
p0.y + (p1.y - p0.y - ts_icon.y) / 2),
ImGui::GetColorU32(ImGuiCol_Text), icon);
ImGui::PopFont();
}
void PrepareComboTitleDescription(const char *label, const char *description,
float combo_size_ratio)
{
float width = ImGui::GetColumnWidth();
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 size(width, GetWidgetTitleDescriptionHeight(label, description));
WidgetTitleDescription(label, description, pos);
ImVec2 wpos = ImGui::GetCursorPos();
ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
ImGui::ItemSize(size, 0.0f);
ImGui::ItemAdd(bb, 0);
ImGui::SetItemAllowOverlap();
ImGui::SameLine(0, 0);
float combo_width = width * combo_size_ratio;
ImGui::SetCursorPos(ImVec2(wpos.x + width - combo_width, wpos.y));
}
bool ChevronCombo(const char *label, int *current_item,
bool (*items_getter)(void *, int, const char **), void *data,
int items_count, const char *description)
{
bool value_changed = false;
float combo_width = ImGui::GetColumnWidth();
if (*label != '#') {
float combo_size_ratio = 0.4;
PrepareComboTitleDescription(label, description, combo_size_ratio);
combo_width *= combo_size_ratio;
}
ImGuiContext& g = *GImGui;
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1, 0));
// Call the getter to obtain the preview string which is a parameter to BeginCombo()
const char* preview_value = NULL;
if (*current_item >= 0 && *current_item < items_count)
items_getter(data, *current_item, &preview_value);
ImGui::SetNextItemWidth(combo_width);
ImGui::PushFont(g_font_mgr.m_menu_font_small);
ImGui::PushID(label);
if (ImGui::BeginCombo("###chevron_combo", preview_value, ImGuiComboFlags_NoArrowButton)) {
// Display items
// FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
for (int i = 0; i < items_count; i++)
{
ImGui::PushID(i);
const bool item_selected = (i == *current_item);
const char* item_text;
if (!items_getter(data, i, &item_text))
item_text = "*Unknown item*";
if (ImGui::Selectable(item_text, item_selected))
{
value_changed = true;
*current_item = i;
}
if (item_selected)
ImGui::SetItemDefaultFocus();
ImGui::PopID();
}
ImGui::EndCombo();
if (value_changed)
ImGui::MarkItemEdited(g.LastItemData.ID);
}
ImGui::PopID();
ImGui::PopFont();
DrawComboChevron();
ImGui::PopStyleVar();
return value_changed;
}
// Getter for the old Combo() API: "item1\0item2\0item3\0"
static bool Items_SingleStringGetter(void* data, int idx, const char** out_text)
{
// FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited.
const char* items_separated_by_zeros = (const char*)data;
int items_count = 0;
const char* p = items_separated_by_zeros;
while (*p)
{
if (idx == items_count)
break;
p += strlen(p) + 1;
items_count++;
}
if (!*p)
return false;
if (out_text)
*out_text = p;
return true;
}
// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
bool ChevronCombo(const char* label, int* current_item, const char* items_separated_by_zeros, const char *description)
{
int items_count = 0;
const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
while (*p)
{
p += strlen(p) + 1;
items_count++;
}
bool value_changed = ChevronCombo(
label, current_item, Items_SingleStringGetter,
(void *)items_separated_by_zeros, items_count, description);
return value_changed;
}
void Hyperlink(const char *text, const char *url)
{
ImColor col;
ImGui::Text("%s", text);
if (ImGui::IsItemHovered()) {
col = IM_COL32_WHITE;
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
} else {
col = ImColor(127, 127, 127, 255);
}
ImVec2 max = ImGui::GetItemRectMax();
ImVec2 min = ImGui::GetItemRectMin();
min.x -= 1 * g_viewport_mgr.m_scale;
min.y = max.y;
max.x -= 1 * g_viewport_mgr.m_scale;
ImGui::GetWindowDrawList()->AddLine(min, max, col, 1.0 * g_viewport_mgr.m_scale);
if (ImGui::IsItemClicked()) {
xemu_open_web_browser(url);
}
}
void HelpMarker(const char* desc)
{
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered())
{
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::TextUnformatted(desc);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}

48
ui/xui/widgets.hh Normal file
View file

@ -0,0 +1,48 @@
//
// xemu User Interface
//
// Copyright (C) 2020-2022 Matt Borgerson
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#pragma once
#include "common.hh"
void Separator();
void SectionTitle(const char *title);
float GetWidgetTitleDescriptionHeight(const char *title,
const char *description);
void WidgetTitleDescription(const char *title, const char *description,
ImVec2 pos);
void WidgetTitleDescriptionItem(const char *str_id,
const char *description = nullptr);
float GetSliderRadius(ImVec2 size);
float GetSliderTrackXOffset(ImVec2 size);
float GetSliderTrackWidth(ImVec2 size);
float GetSliderValueForMousePos(ImVec2 mouse, ImVec2 pos, ImVec2 size);
void DrawSlider(float v, bool hovered, ImVec2 pos, ImVec2 size);
void DrawToggle(bool enabled, bool hovered, ImVec2 pos, ImVec2 size);
bool Toggle(const char *str_id, bool *v, const char *description = nullptr);
void Slider(const char *str_id, float *v, const char *description = nullptr);
bool FilePicker(const char *str_id, const char **buf, const char *filters,
bool dir = false);
void DrawComboChevron();
void PrepareComboTitleDescription(const char *label, const char *description,
float combo_size_ratio);
bool ChevronCombo(const char *label, int *current_item,
bool (*items_getter)(void *, int, const char **), void *data,
int items_count, const char *description = NULL);
bool ChevronCombo(const char* label, int* current_item, const char* items_separated_by_zeros, const char *description = NULL);
void Hyperlink(const char *text, const char *url);
void HelpMarker(const char* desc);

View file

@ -30,7 +30,6 @@ extern "C" {
#endif
// Implemented in xemu.c
extern int scaling_mode;
int xemu_is_fullscreen(void);
void xemu_monitor_init(void);
void xemu_toggle_fullscreen(void);
@ -43,6 +42,7 @@ void xemu_hud_cleanup(void);
void xemu_hud_render(void);
void xemu_hud_process_sdl_events(SDL_Event *event);
void xemu_hud_should_capture_kbd_mouse(int *kbd, int *mouse);
void xemu_hud_set_framebuffer_texture(GLuint tex, bool flip);
#ifdef __cplusplus
}