diff --git a/.clang-format b/.clang-format
index efa5f1c3c4..56804459cb 100644
--- a/.clang-format
+++ b/.clang-format
@@ -68,6 +68,7 @@ IncludeCategories:
 IncludeIsMainRegex: '$'
 IndentCaseLabels: false
 IndentWidth:     4
+AccessModifierOffset: -4
 IndentWrappedFunctionNames: false
 KeepEmptyLinesAtTheStartOfBlocks: false
 MacroBlockBegin: '.*_BEGIN$' # only PREC_BEGIN ?
diff --git a/.gitmodules b/.gitmodules
index 7b390efe57..699d5526f7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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"]
diff --git a/build.sh b/build.sh
index 0163be239d..10d40de3bc 100755
--- a/build.sh
+++ b/build.sh
@@ -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
diff --git a/config_spec.yml b/config_spec.yml
index d58b01e1f3..b1f7e9d538 100644
--- a/config_spec.yml
+++ b/config_spec.yml
@@ -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
diff --git a/configure b/configure
index f71e805409..34fea06dba 100755
--- a/configure
+++ b/configure
@@ -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.
diff --git a/data/roboto_medium.ttf b/data/Roboto-Medium.ttf
similarity index 97%
rename from data/roboto_medium.ttf
rename to data/Roboto-Medium.ttf
index f714a514d9..e89b0b79a2 100644
Binary files a/data/roboto_medium.ttf and b/data/Roboto-Medium.ttf differ
diff --git a/data/RobotoCondensed-Regular.ttf b/data/RobotoCondensed-Regular.ttf
new file mode 100644
index 0000000000..17e8ea57b1
Binary files /dev/null and b/data/RobotoCondensed-Regular.ttf differ
diff --git a/data/abxy.svg b/data/abxy.svg
new file mode 100644
index 0000000000..9adde380e1
--- /dev/null
+++ b/data/abxy.svg
@@ -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>
diff --git a/data/abxy.ttf b/data/abxy.ttf
new file mode 100644
index 0000000000..d7b4b2f1d1
Binary files /dev/null and b/data/abxy.ttf differ
diff --git a/data/abxy_font.svg b/data/abxy_font.svg
new file mode 100644
index 0000000000..caaeb154ce
--- /dev/null
+++ b/data/abxy_font.svg
@@ -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>
diff --git a/data/font_awesome_6_1_1_solid.otf b/data/font_awesome_6_1_1_solid.otf
new file mode 100644
index 0000000000..2452af5832
Binary files /dev/null and b/data/font_awesome_6_1_1_solid.otf differ
diff --git a/data/meson.build b/data/meson.build
index 785e335a40..89e6123d90 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -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 = []
diff --git a/genconfig b/genconfig
index 8220e87489..5da3fd2463 160000
--- a/genconfig
+++ b/genconfig
@@ -1 +1 @@
-Subproject commit 8220e8748922ddd9bba4cbacd608601586c9a4bb
+Subproject commit 5da3fd2463288d9e048dbf3ea41f2bad0a4287a8
diff --git a/hw/xbox/mcpx/apu.c b/hw/xbox/mcpx/apu.c
index 1344793337..cf34f14197 100644
--- a/hw/xbox/mcpx/apu.c
+++ b/hw/xbox/mcpx/apu.c
@@ -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);
diff --git a/licenses/fontawesome.license.txt b/licenses/fontawesome.license.txt
new file mode 100644
index 0000000000..cc557ece45
--- /dev/null
+++ b/licenses/fontawesome.license.txt
@@ -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.**
diff --git a/licenses/fpng.license.txt b/licenses/fpng.license.txt
new file mode 100644
index 0000000000..68a49daad8
--- /dev/null
+++ b/licenses/fpng.license.txt
@@ -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/>
diff --git a/licenses/tomlplusplus.license.txt b/licenses/tomlplusplus.license.txt
new file mode 100644
index 0000000000..261cd61587
--- /dev/null
+++ b/licenses/tomlplusplus.license.txt
@@ -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.
diff --git a/net/slirp.c b/net/slirp.c
index ad3a838e0b..d5f6d621c1 100644
--- a/net/slirp.c
+++ b/net/slirp.c
@@ -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 };
diff --git a/scripts/archive-source.sh b/scripts/archive-source.sh
index a2044943d2..debb1d344d 100755
--- a/scripts/archive-source.sh
+++ b/scripts/archive-source.sh
@@ -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() {
diff --git a/scripts/gen-license.py b/scripts/gen-license.py
index 705f8f96a1..66ec79cb98 100755
--- a/scripts/gen-license.py
+++ b/scripts/gen-license.py
@@ -20,6 +20,7 @@ bsd_3clause = 'bsd-3clause'
 zlib = 'zlib'
 lgplv2_1 = 'lgplv2_1'
 apache2 = 'apache2'
+unlicense = 'unlicense'
 multi = 'multi'
 
 
@@ -179,13 +180,13 @@ Lib('slirp', 'https://gitlab.freedesktop.org/slirp',
 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 @@ Lib('stb_image', 'https://github.com/nothings/stb',
 	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 @@ Lib('xxHash', 'https://github.com/Cyan4973/xxHash.git',
 	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 @@ Lib('roboto', 'https://github.com/googlefonts/roboto',
 	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
 #
diff --git a/softmmu/vl.c b/softmmu/vl.c
index 0a76d0335c..2eecce1cd7 100644
--- a/softmmu/vl.c
+++ b/softmmu/vl.c
@@ -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) {
diff --git a/ui/imgui b/ui/imgui
deleted file mode 160000
index e18abe3619..0000000000
--- a/ui/imgui
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit e18abe3619cfa0eced163c027d0349506814816c
diff --git a/ui/implot b/ui/implot
deleted file mode 160000
index a6bab98517..0000000000
--- a/ui/implot
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a6bab98517b1baa3116db52518dda1eb2d7eaab7
diff --git a/ui/meson.build b/ui/meson.build
index d73512360f..02064602e7 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -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)
 
diff --git a/ui/thirdparty/fa/IconsFontAwesome6.h b/ui/thirdparty/fa/IconsFontAwesome6.h
new file mode 100644
index 0000000000..050956448c
--- /dev/null
+++ b/ui/thirdparty/fa/IconsFontAwesome6.h
@@ -0,0 +1,1393 @@
+// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py for languages C and C++
+// from https://github.com/FortAwesome/Font-Awesome/raw/6.x/metadata/icons.yml
+// for use with https://github.com/FortAwesome/Font-Awesome/blob/6.x/webfonts/fa-regular-400.ttf, https://github.com/FortAwesome/Font-Awesome/blob/6.x/webfonts/fa-solid-900.ttf
+#pragma once
+
+#define FONT_ICON_FILE_NAME_FAR "fa-regular-400.ttf"
+#define FONT_ICON_FILE_NAME_FAS "fa-solid-900.ttf"
+
+#define ICON_MIN_FA 0x21
+#define ICON_MAX_FA 0xf8ff
+#define ICON_FA_0 "0"	// U+30
+#define ICON_FA_1 "1"	// U+31
+#define ICON_FA_2 "2"	// U+32
+#define ICON_FA_3 "3"	// U+33
+#define ICON_FA_4 "4"	// U+34
+#define ICON_FA_5 "5"	// U+35
+#define ICON_FA_6 "6"	// U+36
+#define ICON_FA_7 "7"	// U+37
+#define ICON_FA_8 "8"	// U+38
+#define ICON_FA_9 "9"	// U+39
+#define ICON_FA_A "A"	// U+41
+#define ICON_FA_ADDRESS_BOOK "\xef\x8a\xb9"	// U+f2b9
+#define ICON_FA_ADDRESS_CARD "\xef\x8a\xbb"	// U+f2bb
+#define ICON_FA_ALIGN_CENTER "\xef\x80\xb7"	// U+f037
+#define ICON_FA_ALIGN_JUSTIFY "\xef\x80\xb9"	// U+f039
+#define ICON_FA_ALIGN_LEFT "\xef\x80\xb6"	// U+f036
+#define ICON_FA_ALIGN_RIGHT "\xef\x80\xb8"	// U+f038
+#define ICON_FA_ANCHOR "\xef\x84\xbd"	// U+f13d
+#define ICON_FA_ANCHOR_CIRCLE_CHECK "\xee\x92\xaa"	// U+e4aa
+#define ICON_FA_ANCHOR_CIRCLE_EXCLAMATION "\xee\x92\xab"	// U+e4ab
+#define ICON_FA_ANCHOR_CIRCLE_XMARK "\xee\x92\xac"	// U+e4ac
+#define ICON_FA_ANCHOR_LOCK "\xee\x92\xad"	// U+e4ad
+#define ICON_FA_ANGLE_DOWN "\xef\x84\x87"	// U+f107
+#define ICON_FA_ANGLE_LEFT "\xef\x84\x84"	// U+f104
+#define ICON_FA_ANGLE_RIGHT "\xef\x84\x85"	// U+f105
+#define ICON_FA_ANGLE_UP "\xef\x84\x86"	// U+f106
+#define ICON_FA_ANGLES_DOWN "\xef\x84\x83"	// U+f103
+#define ICON_FA_ANGLES_LEFT "\xef\x84\x80"	// U+f100
+#define ICON_FA_ANGLES_RIGHT "\xef\x84\x81"	// U+f101
+#define ICON_FA_ANGLES_UP "\xef\x84\x82"	// U+f102
+#define ICON_FA_ANKH "\xef\x99\x84"	// U+f644
+#define ICON_FA_APPLE_WHOLE "\xef\x97\x91"	// U+f5d1
+#define ICON_FA_ARCHWAY "\xef\x95\x97"	// U+f557
+#define ICON_FA_ARROW_DOWN "\xef\x81\xa3"	// U+f063
+#define ICON_FA_ARROW_DOWN_1_9 "\xef\x85\xa2"	// U+f162
+#define ICON_FA_ARROW_DOWN_9_1 "\xef\xa2\x86"	// U+f886
+#define ICON_FA_ARROW_DOWN_A_Z "\xef\x85\x9d"	// U+f15d
+#define ICON_FA_ARROW_DOWN_LONG "\xef\x85\xb5"	// U+f175
+#define ICON_FA_ARROW_DOWN_SHORT_WIDE "\xef\xa2\x84"	// U+f884
+#define ICON_FA_ARROW_DOWN_UP_ACROSS_LINE "\xee\x92\xaf"	// U+e4af
+#define ICON_FA_ARROW_DOWN_UP_LOCK "\xee\x92\xb0"	// U+e4b0
+#define ICON_FA_ARROW_DOWN_WIDE_SHORT "\xef\x85\xa0"	// U+f160
+#define ICON_FA_ARROW_DOWN_Z_A "\xef\xa2\x81"	// U+f881
+#define ICON_FA_ARROW_LEFT "\xef\x81\xa0"	// U+f060
+#define ICON_FA_ARROW_LEFT_LONG "\xef\x85\xb7"	// U+f177
+#define ICON_FA_ARROW_POINTER "\xef\x89\x85"	// U+f245
+#define ICON_FA_ARROW_RIGHT "\xef\x81\xa1"	// U+f061
+#define ICON_FA_ARROW_RIGHT_ARROW_LEFT "\xef\x83\xac"	// U+f0ec
+#define ICON_FA_ARROW_RIGHT_FROM_BRACKET "\xef\x82\x8b"	// U+f08b
+#define ICON_FA_ARROW_RIGHT_LONG "\xef\x85\xb8"	// U+f178
+#define ICON_FA_ARROW_RIGHT_TO_BRACKET "\xef\x82\x90"	// U+f090
+#define ICON_FA_ARROW_RIGHT_TO_CITY "\xee\x92\xb3"	// U+e4b3
+#define ICON_FA_ARROW_ROTATE_LEFT "\xef\x83\xa2"	// U+f0e2
+#define ICON_FA_ARROW_ROTATE_RIGHT "\xef\x80\x9e"	// U+f01e
+#define ICON_FA_ARROW_TREND_DOWN "\xee\x82\x97"	// U+e097
+#define ICON_FA_ARROW_TREND_UP "\xee\x82\x98"	// U+e098
+#define ICON_FA_ARROW_TURN_DOWN "\xef\x85\x89"	// U+f149
+#define ICON_FA_ARROW_TURN_UP "\xef\x85\x88"	// U+f148
+#define ICON_FA_ARROW_UP "\xef\x81\xa2"	// U+f062
+#define ICON_FA_ARROW_UP_1_9 "\xef\x85\xa3"	// U+f163
+#define ICON_FA_ARROW_UP_9_1 "\xef\xa2\x87"	// U+f887
+#define ICON_FA_ARROW_UP_A_Z "\xef\x85\x9e"	// U+f15e
+#define ICON_FA_ARROW_UP_FROM_BRACKET "\xee\x82\x9a"	// U+e09a
+#define ICON_FA_ARROW_UP_FROM_GROUND_WATER "\xee\x92\xb5"	// U+e4b5
+#define ICON_FA_ARROW_UP_FROM_WATER_PUMP "\xee\x92\xb6"	// U+e4b6
+#define ICON_FA_ARROW_UP_LONG "\xef\x85\xb6"	// U+f176
+#define ICON_FA_ARROW_UP_RIGHT_DOTS "\xee\x92\xb7"	// U+e4b7
+#define ICON_FA_ARROW_UP_RIGHT_FROM_SQUARE "\xef\x82\x8e"	// U+f08e
+#define ICON_FA_ARROW_UP_SHORT_WIDE "\xef\xa2\x85"	// U+f885
+#define ICON_FA_ARROW_UP_WIDE_SHORT "\xef\x85\xa1"	// U+f161
+#define ICON_FA_ARROW_UP_Z_A "\xef\xa2\x82"	// U+f882
+#define ICON_FA_ARROWS_DOWN_TO_LINE "\xee\x92\xb8"	// U+e4b8
+#define ICON_FA_ARROWS_DOWN_TO_PEOPLE "\xee\x92\xb9"	// U+e4b9
+#define ICON_FA_ARROWS_LEFT_RIGHT "\xef\x81\xbe"	// U+f07e
+#define ICON_FA_ARROWS_LEFT_RIGHT_TO_LINE "\xee\x92\xba"	// U+e4ba
+#define ICON_FA_ARROWS_ROTATE "\xef\x80\xa1"	// U+f021
+#define ICON_FA_ARROWS_SPIN "\xee\x92\xbb"	// U+e4bb
+#define ICON_FA_ARROWS_SPLIT_UP_AND_LEFT "\xee\x92\xbc"	// U+e4bc
+#define ICON_FA_ARROWS_TO_CIRCLE "\xee\x92\xbd"	// U+e4bd
+#define ICON_FA_ARROWS_TO_DOT "\xee\x92\xbe"	// U+e4be
+#define ICON_FA_ARROWS_TO_EYE "\xee\x92\xbf"	// U+e4bf
+#define ICON_FA_ARROWS_TURN_RIGHT "\xee\x93\x80"	// U+e4c0
+#define ICON_FA_ARROWS_TURN_TO_DOTS "\xee\x93\x81"	// U+e4c1
+#define ICON_FA_ARROWS_UP_DOWN "\xef\x81\xbd"	// U+f07d
+#define ICON_FA_ARROWS_UP_DOWN_LEFT_RIGHT "\xef\x81\x87"	// U+f047
+#define ICON_FA_ARROWS_UP_TO_LINE "\xee\x93\x82"	// U+e4c2
+#define ICON_FA_ASTERISK "*"	// U+2a
+#define ICON_FA_AT "@"	// U+40
+#define ICON_FA_ATOM "\xef\x97\x92"	// U+f5d2
+#define ICON_FA_AUDIO_DESCRIPTION "\xef\x8a\x9e"	// U+f29e
+#define ICON_FA_AUSTRAL_SIGN "\xee\x82\xa9"	// U+e0a9
+#define ICON_FA_AWARD "\xef\x95\x99"	// U+f559
+#define ICON_FA_B "B"	// U+42
+#define ICON_FA_BABY "\xef\x9d\xbc"	// U+f77c
+#define ICON_FA_BABY_CARRIAGE "\xef\x9d\xbd"	// U+f77d
+#define ICON_FA_BACKWARD "\xef\x81\x8a"	// U+f04a
+#define ICON_FA_BACKWARD_FAST "\xef\x81\x89"	// U+f049
+#define ICON_FA_BACKWARD_STEP "\xef\x81\x88"	// U+f048
+#define ICON_FA_BACON "\xef\x9f\xa5"	// U+f7e5
+#define ICON_FA_BACTERIA "\xee\x81\x99"	// U+e059
+#define ICON_FA_BACTERIUM "\xee\x81\x9a"	// U+e05a
+#define ICON_FA_BAG_SHOPPING "\xef\x8a\x90"	// U+f290
+#define ICON_FA_BAHAI "\xef\x99\xa6"	// U+f666
+#define ICON_FA_BAHT_SIGN "\xee\x82\xac"	// U+e0ac
+#define ICON_FA_BAN "\xef\x81\x9e"	// U+f05e
+#define ICON_FA_BAN_SMOKING "\xef\x95\x8d"	// U+f54d
+#define ICON_FA_BANDAGE "\xef\x91\xa2"	// U+f462
+#define ICON_FA_BARCODE "\xef\x80\xaa"	// U+f02a
+#define ICON_FA_BARS "\xef\x83\x89"	// U+f0c9
+#define ICON_FA_BARS_PROGRESS "\xef\xa0\xa8"	// U+f828
+#define ICON_FA_BARS_STAGGERED "\xef\x95\x90"	// U+f550
+#define ICON_FA_BASEBALL "\xef\x90\xb3"	// U+f433
+#define ICON_FA_BASEBALL_BAT_BALL "\xef\x90\xb2"	// U+f432
+#define ICON_FA_BASKET_SHOPPING "\xef\x8a\x91"	// U+f291
+#define ICON_FA_BASKETBALL "\xef\x90\xb4"	// U+f434
+#define ICON_FA_BATH "\xef\x8b\x8d"	// U+f2cd
+#define ICON_FA_BATTERY_EMPTY "\xef\x89\x84"	// U+f244
+#define ICON_FA_BATTERY_FULL "\xef\x89\x80"	// U+f240
+#define ICON_FA_BATTERY_HALF "\xef\x89\x82"	// U+f242
+#define ICON_FA_BATTERY_QUARTER "\xef\x89\x83"	// U+f243
+#define ICON_FA_BATTERY_THREE_QUARTERS "\xef\x89\x81"	// U+f241
+#define ICON_FA_BED "\xef\x88\xb6"	// U+f236
+#define ICON_FA_BED_PULSE "\xef\x92\x87"	// U+f487
+#define ICON_FA_BEER_MUG_EMPTY "\xef\x83\xbc"	// U+f0fc
+#define ICON_FA_BELL "\xef\x83\xb3"	// U+f0f3
+#define ICON_FA_BELL_CONCIERGE "\xef\x95\xa2"	// U+f562
+#define ICON_FA_BELL_SLASH "\xef\x87\xb6"	// U+f1f6
+#define ICON_FA_BEZIER_CURVE "\xef\x95\x9b"	// U+f55b
+#define ICON_FA_BICYCLE "\xef\x88\x86"	// U+f206
+#define ICON_FA_BINOCULARS "\xef\x87\xa5"	// U+f1e5
+#define ICON_FA_BIOHAZARD "\xef\x9e\x80"	// U+f780
+#define ICON_FA_BITCOIN_SIGN "\xee\x82\xb4"	// U+e0b4
+#define ICON_FA_BLENDER "\xef\x94\x97"	// U+f517
+#define ICON_FA_BLENDER_PHONE "\xef\x9a\xb6"	// U+f6b6
+#define ICON_FA_BLOG "\xef\x9e\x81"	// U+f781
+#define ICON_FA_BOLD "\xef\x80\xb2"	// U+f032
+#define ICON_FA_BOLT "\xef\x83\xa7"	// U+f0e7
+#define ICON_FA_BOLT_LIGHTNING "\xee\x82\xb7"	// U+e0b7
+#define ICON_FA_BOMB "\xef\x87\xa2"	// U+f1e2
+#define ICON_FA_BONE "\xef\x97\x97"	// U+f5d7
+#define ICON_FA_BONG "\xef\x95\x9c"	// U+f55c
+#define ICON_FA_BOOK "\xef\x80\xad"	// U+f02d
+#define ICON_FA_BOOK_ATLAS "\xef\x95\x98"	// U+f558
+#define ICON_FA_BOOK_BIBLE "\xef\x99\x87"	// U+f647
+#define ICON_FA_BOOK_BOOKMARK "\xee\x82\xbb"	// U+e0bb
+#define ICON_FA_BOOK_JOURNAL_WHILLS "\xef\x99\xaa"	// U+f66a
+#define ICON_FA_BOOK_MEDICAL "\xef\x9f\xa6"	// U+f7e6
+#define ICON_FA_BOOK_OPEN "\xef\x94\x98"	// U+f518
+#define ICON_FA_BOOK_OPEN_READER "\xef\x97\x9a"	// U+f5da
+#define ICON_FA_BOOK_QURAN "\xef\x9a\x87"	// U+f687
+#define ICON_FA_BOOK_SKULL "\xef\x9a\xb7"	// U+f6b7
+#define ICON_FA_BOOKMARK "\xef\x80\xae"	// U+f02e
+#define ICON_FA_BORDER_ALL "\xef\xa1\x8c"	// U+f84c
+#define ICON_FA_BORDER_NONE "\xef\xa1\x90"	// U+f850
+#define ICON_FA_BORDER_TOP_LEFT "\xef\xa1\x93"	// U+f853
+#define ICON_FA_BORE_HOLE "\xee\x93\x83"	// U+e4c3
+#define ICON_FA_BOTTLE_DROPLET "\xee\x93\x84"	// U+e4c4
+#define ICON_FA_BOTTLE_WATER "\xee\x93\x85"	// U+e4c5
+#define ICON_FA_BOWL_FOOD "\xee\x93\x86"	// U+e4c6
+#define ICON_FA_BOWL_RICE "\xee\x8b\xab"	// U+e2eb
+#define ICON_FA_BOWLING_BALL "\xef\x90\xb6"	// U+f436
+#define ICON_FA_BOX "\xef\x91\xa6"	// U+f466
+#define ICON_FA_BOX_ARCHIVE "\xef\x86\x87"	// U+f187
+#define ICON_FA_BOX_OPEN "\xef\x92\x9e"	// U+f49e
+#define ICON_FA_BOX_TISSUE "\xee\x81\x9b"	// U+e05b
+#define ICON_FA_BOXES_PACKING "\xee\x93\x87"	// U+e4c7
+#define ICON_FA_BOXES_STACKED "\xef\x91\xa8"	// U+f468
+#define ICON_FA_BRAILLE "\xef\x8a\xa1"	// U+f2a1
+#define ICON_FA_BRAIN "\xef\x97\x9c"	// U+f5dc
+#define ICON_FA_BRAZILIAN_REAL_SIGN "\xee\x91\xac"	// U+e46c
+#define ICON_FA_BREAD_SLICE "\xef\x9f\xac"	// U+f7ec
+#define ICON_FA_BRIDGE "\xee\x93\x88"	// U+e4c8
+#define ICON_FA_BRIDGE_CIRCLE_CHECK "\xee\x93\x89"	// U+e4c9
+#define ICON_FA_BRIDGE_CIRCLE_EXCLAMATION "\xee\x93\x8a"	// U+e4ca
+#define ICON_FA_BRIDGE_CIRCLE_XMARK "\xee\x93\x8b"	// U+e4cb
+#define ICON_FA_BRIDGE_LOCK "\xee\x93\x8c"	// U+e4cc
+#define ICON_FA_BRIDGE_WATER "\xee\x93\x8e"	// U+e4ce
+#define ICON_FA_BRIEFCASE "\xef\x82\xb1"	// U+f0b1
+#define ICON_FA_BRIEFCASE_MEDICAL "\xef\x91\xa9"	// U+f469
+#define ICON_FA_BROOM "\xef\x94\x9a"	// U+f51a
+#define ICON_FA_BROOM_BALL "\xef\x91\x98"	// U+f458
+#define ICON_FA_BRUSH "\xef\x95\x9d"	// U+f55d
+#define ICON_FA_BUCKET "\xee\x93\x8f"	// U+e4cf
+#define ICON_FA_BUG "\xef\x86\x88"	// U+f188
+#define ICON_FA_BUG_SLASH "\xee\x92\x90"	// U+e490
+#define ICON_FA_BUGS "\xee\x93\x90"	// U+e4d0
+#define ICON_FA_BUILDING "\xef\x86\xad"	// U+f1ad
+#define ICON_FA_BUILDING_CIRCLE_ARROW_RIGHT "\xee\x93\x91"	// U+e4d1
+#define ICON_FA_BUILDING_CIRCLE_CHECK "\xee\x93\x92"	// U+e4d2
+#define ICON_FA_BUILDING_CIRCLE_EXCLAMATION "\xee\x93\x93"	// U+e4d3
+#define ICON_FA_BUILDING_CIRCLE_XMARK "\xee\x93\x94"	// U+e4d4
+#define ICON_FA_BUILDING_COLUMNS "\xef\x86\x9c"	// U+f19c
+#define ICON_FA_BUILDING_FLAG "\xee\x93\x95"	// U+e4d5
+#define ICON_FA_BUILDING_LOCK "\xee\x93\x96"	// U+e4d6
+#define ICON_FA_BUILDING_NGO "\xee\x93\x97"	// U+e4d7
+#define ICON_FA_BUILDING_SHIELD "\xee\x93\x98"	// U+e4d8
+#define ICON_FA_BUILDING_UN "\xee\x93\x99"	// U+e4d9
+#define ICON_FA_BUILDING_USER "\xee\x93\x9a"	// U+e4da
+#define ICON_FA_BUILDING_WHEAT "\xee\x93\x9b"	// U+e4db
+#define ICON_FA_BULLHORN "\xef\x82\xa1"	// U+f0a1
+#define ICON_FA_BULLSEYE "\xef\x85\x80"	// U+f140
+#define ICON_FA_BURGER "\xef\xa0\x85"	// U+f805
+#define ICON_FA_BURST "\xee\x93\x9c"	// U+e4dc
+#define ICON_FA_BUS "\xef\x88\x87"	// U+f207
+#define ICON_FA_BUS_SIMPLE "\xef\x95\x9e"	// U+f55e
+#define ICON_FA_BUSINESS_TIME "\xef\x99\x8a"	// U+f64a
+#define ICON_FA_C "C"	// U+43
+#define ICON_FA_CAKE_CANDLES "\xef\x87\xbd"	// U+f1fd
+#define ICON_FA_CALCULATOR "\xef\x87\xac"	// U+f1ec
+#define ICON_FA_CALENDAR "\xef\x84\xb3"	// U+f133
+#define ICON_FA_CALENDAR_CHECK "\xef\x89\xb4"	// U+f274
+#define ICON_FA_CALENDAR_DAY "\xef\x9e\x83"	// U+f783
+#define ICON_FA_CALENDAR_DAYS "\xef\x81\xb3"	// U+f073
+#define ICON_FA_CALENDAR_MINUS "\xef\x89\xb2"	// U+f272
+#define ICON_FA_CALENDAR_PLUS "\xef\x89\xb1"	// U+f271
+#define ICON_FA_CALENDAR_WEEK "\xef\x9e\x84"	// U+f784
+#define ICON_FA_CALENDAR_XMARK "\xef\x89\xb3"	// U+f273
+#define ICON_FA_CAMERA "\xef\x80\xb0"	// U+f030
+#define ICON_FA_CAMERA_RETRO "\xef\x82\x83"	// U+f083
+#define ICON_FA_CAMERA_ROTATE "\xee\x83\x98"	// U+e0d8
+#define ICON_FA_CAMPGROUND "\xef\x9a\xbb"	// U+f6bb
+#define ICON_FA_CANDY_CANE "\xef\x9e\x86"	// U+f786
+#define ICON_FA_CANNABIS "\xef\x95\x9f"	// U+f55f
+#define ICON_FA_CAPSULES "\xef\x91\xab"	// U+f46b
+#define ICON_FA_CAR "\xef\x86\xb9"	// U+f1b9
+#define ICON_FA_CAR_BATTERY "\xef\x97\x9f"	// U+f5df
+#define ICON_FA_CAR_BURST "\xef\x97\xa1"	// U+f5e1
+#define ICON_FA_CAR_ON "\xee\x93\x9d"	// U+e4dd
+#define ICON_FA_CAR_REAR "\xef\x97\x9e"	// U+f5de
+#define ICON_FA_CAR_SIDE "\xef\x97\xa4"	// U+f5e4
+#define ICON_FA_CAR_TUNNEL "\xee\x93\x9e"	// U+e4de
+#define ICON_FA_CARAVAN "\xef\xa3\xbf"	// U+f8ff
+#define ICON_FA_CARET_DOWN "\xef\x83\x97"	// U+f0d7
+#define ICON_FA_CARET_LEFT "\xef\x83\x99"	// U+f0d9
+#define ICON_FA_CARET_RIGHT "\xef\x83\x9a"	// U+f0da
+#define ICON_FA_CARET_UP "\xef\x83\x98"	// U+f0d8
+#define ICON_FA_CARROT "\xef\x9e\x87"	// U+f787
+#define ICON_FA_CART_ARROW_DOWN "\xef\x88\x98"	// U+f218
+#define ICON_FA_CART_FLATBED "\xef\x91\xb4"	// U+f474
+#define ICON_FA_CART_FLATBED_SUITCASE "\xef\x96\x9d"	// U+f59d
+#define ICON_FA_CART_PLUS "\xef\x88\x97"	// U+f217
+#define ICON_FA_CART_SHOPPING "\xef\x81\xba"	// U+f07a
+#define ICON_FA_CASH_REGISTER "\xef\x9e\x88"	// U+f788
+#define ICON_FA_CAT "\xef\x9a\xbe"	// U+f6be
+#define ICON_FA_CEDI_SIGN "\xee\x83\x9f"	// U+e0df
+#define ICON_FA_CENT_SIGN "\xee\x8f\xb5"	// U+e3f5
+#define ICON_FA_CERTIFICATE "\xef\x82\xa3"	// U+f0a3
+#define ICON_FA_CHAIR "\xef\x9b\x80"	// U+f6c0
+#define ICON_FA_CHALKBOARD "\xef\x94\x9b"	// U+f51b
+#define ICON_FA_CHALKBOARD_USER "\xef\x94\x9c"	// U+f51c
+#define ICON_FA_CHAMPAGNE_GLASSES "\xef\x9e\x9f"	// U+f79f
+#define ICON_FA_CHARGING_STATION "\xef\x97\xa7"	// U+f5e7
+#define ICON_FA_CHART_AREA "\xef\x87\xbe"	// U+f1fe
+#define ICON_FA_CHART_BAR "\xef\x82\x80"	// U+f080
+#define ICON_FA_CHART_COLUMN "\xee\x83\xa3"	// U+e0e3
+#define ICON_FA_CHART_GANTT "\xee\x83\xa4"	// U+e0e4
+#define ICON_FA_CHART_LINE "\xef\x88\x81"	// U+f201
+#define ICON_FA_CHART_PIE "\xef\x88\x80"	// U+f200
+#define ICON_FA_CHART_SIMPLE "\xee\x91\xb3"	// U+e473
+#define ICON_FA_CHECK "\xef\x80\x8c"	// U+f00c
+#define ICON_FA_CHECK_DOUBLE "\xef\x95\xa0"	// U+f560
+#define ICON_FA_CHECK_TO_SLOT "\xef\x9d\xb2"	// U+f772
+#define ICON_FA_CHEESE "\xef\x9f\xaf"	// U+f7ef
+#define ICON_FA_CHESS "\xef\x90\xb9"	// U+f439
+#define ICON_FA_CHESS_BISHOP "\xef\x90\xba"	// U+f43a
+#define ICON_FA_CHESS_BOARD "\xef\x90\xbc"	// U+f43c
+#define ICON_FA_CHESS_KING "\xef\x90\xbf"	// U+f43f
+#define ICON_FA_CHESS_KNIGHT "\xef\x91\x81"	// U+f441
+#define ICON_FA_CHESS_PAWN "\xef\x91\x83"	// U+f443
+#define ICON_FA_CHESS_QUEEN "\xef\x91\x85"	// U+f445
+#define ICON_FA_CHESS_ROOK "\xef\x91\x87"	// U+f447
+#define ICON_FA_CHEVRON_DOWN "\xef\x81\xb8"	// U+f078
+#define ICON_FA_CHEVRON_LEFT "\xef\x81\x93"	// U+f053
+#define ICON_FA_CHEVRON_RIGHT "\xef\x81\x94"	// U+f054
+#define ICON_FA_CHEVRON_UP "\xef\x81\xb7"	// U+f077
+#define ICON_FA_CHILD "\xef\x86\xae"	// U+f1ae
+#define ICON_FA_CHILD_RIFLE "\xee\x93\xa0"	// U+e4e0
+#define ICON_FA_CHILDREN "\xee\x93\xa1"	// U+e4e1
+#define ICON_FA_CHURCH "\xef\x94\x9d"	// U+f51d
+#define ICON_FA_CIRCLE "\xef\x84\x91"	// U+f111
+#define ICON_FA_CIRCLE_ARROW_DOWN "\xef\x82\xab"	// U+f0ab
+#define ICON_FA_CIRCLE_ARROW_LEFT "\xef\x82\xa8"	// U+f0a8
+#define ICON_FA_CIRCLE_ARROW_RIGHT "\xef\x82\xa9"	// U+f0a9
+#define ICON_FA_CIRCLE_ARROW_UP "\xef\x82\xaa"	// U+f0aa
+#define ICON_FA_CIRCLE_CHECK "\xef\x81\x98"	// U+f058
+#define ICON_FA_CIRCLE_CHEVRON_DOWN "\xef\x84\xba"	// U+f13a
+#define ICON_FA_CIRCLE_CHEVRON_LEFT "\xef\x84\xb7"	// U+f137
+#define ICON_FA_CIRCLE_CHEVRON_RIGHT "\xef\x84\xb8"	// U+f138
+#define ICON_FA_CIRCLE_CHEVRON_UP "\xef\x84\xb9"	// U+f139
+#define ICON_FA_CIRCLE_DOLLAR_TO_SLOT "\xef\x92\xb9"	// U+f4b9
+#define ICON_FA_CIRCLE_DOT "\xef\x86\x92"	// U+f192
+#define ICON_FA_CIRCLE_DOWN "\xef\x8d\x98"	// U+f358
+#define ICON_FA_CIRCLE_EXCLAMATION "\xef\x81\xaa"	// U+f06a
+#define ICON_FA_CIRCLE_H "\xef\x91\xbe"	// U+f47e
+#define ICON_FA_CIRCLE_HALF_STROKE "\xef\x81\x82"	// U+f042
+#define ICON_FA_CIRCLE_INFO "\xef\x81\x9a"	// U+f05a
+#define ICON_FA_CIRCLE_LEFT "\xef\x8d\x99"	// U+f359
+#define ICON_FA_CIRCLE_MINUS "\xef\x81\x96"	// U+f056
+#define ICON_FA_CIRCLE_NODES "\xee\x93\xa2"	// U+e4e2
+#define ICON_FA_CIRCLE_NOTCH "\xef\x87\x8e"	// U+f1ce
+#define ICON_FA_CIRCLE_PAUSE "\xef\x8a\x8b"	// U+f28b
+#define ICON_FA_CIRCLE_PLAY "\xef\x85\x84"	// U+f144
+#define ICON_FA_CIRCLE_PLUS "\xef\x81\x95"	// U+f055
+#define ICON_FA_CIRCLE_QUESTION "\xef\x81\x99"	// U+f059
+#define ICON_FA_CIRCLE_RADIATION "\xef\x9e\xba"	// U+f7ba
+#define ICON_FA_CIRCLE_RIGHT "\xef\x8d\x9a"	// U+f35a
+#define ICON_FA_CIRCLE_STOP "\xef\x8a\x8d"	// U+f28d
+#define ICON_FA_CIRCLE_UP "\xef\x8d\x9b"	// U+f35b
+#define ICON_FA_CIRCLE_USER "\xef\x8a\xbd"	// U+f2bd
+#define ICON_FA_CIRCLE_XMARK "\xef\x81\x97"	// U+f057
+#define ICON_FA_CITY "\xef\x99\x8f"	// U+f64f
+#define ICON_FA_CLAPPERBOARD "\xee\x84\xb1"	// U+e131
+#define ICON_FA_CLIPBOARD "\xef\x8c\xa8"	// U+f328
+#define ICON_FA_CLIPBOARD_CHECK "\xef\x91\xac"	// U+f46c
+#define ICON_FA_CLIPBOARD_LIST "\xef\x91\xad"	// U+f46d
+#define ICON_FA_CLIPBOARD_QUESTION "\xee\x93\xa3"	// U+e4e3
+#define ICON_FA_CLIPBOARD_USER "\xef\x9f\xb3"	// U+f7f3
+#define ICON_FA_CLOCK "\xef\x80\x97"	// U+f017
+#define ICON_FA_CLOCK_ROTATE_LEFT "\xef\x87\x9a"	// U+f1da
+#define ICON_FA_CLONE "\xef\x89\x8d"	// U+f24d
+#define ICON_FA_CLOSED_CAPTIONING "\xef\x88\x8a"	// U+f20a
+#define ICON_FA_CLOUD "\xef\x83\x82"	// U+f0c2
+#define ICON_FA_CLOUD_ARROW_DOWN "\xef\x83\xad"	// U+f0ed
+#define ICON_FA_CLOUD_ARROW_UP "\xef\x83\xae"	// U+f0ee
+#define ICON_FA_CLOUD_BOLT "\xef\x9d\xac"	// U+f76c
+#define ICON_FA_CLOUD_MEATBALL "\xef\x9c\xbb"	// U+f73b
+#define ICON_FA_CLOUD_MOON "\xef\x9b\x83"	// U+f6c3
+#define ICON_FA_CLOUD_MOON_RAIN "\xef\x9c\xbc"	// U+f73c
+#define ICON_FA_CLOUD_RAIN "\xef\x9c\xbd"	// U+f73d
+#define ICON_FA_CLOUD_SHOWERS_HEAVY "\xef\x9d\x80"	// U+f740
+#define ICON_FA_CLOUD_SHOWERS_WATER "\xee\x93\xa4"	// U+e4e4
+#define ICON_FA_CLOUD_SUN "\xef\x9b\x84"	// U+f6c4
+#define ICON_FA_CLOUD_SUN_RAIN "\xef\x9d\x83"	// U+f743
+#define ICON_FA_CLOVER "\xee\x84\xb9"	// U+e139
+#define ICON_FA_CODE "\xef\x84\xa1"	// U+f121
+#define ICON_FA_CODE_BRANCH "\xef\x84\xa6"	// U+f126
+#define ICON_FA_CODE_COMMIT "\xef\x8e\x86"	// U+f386
+#define ICON_FA_CODE_COMPARE "\xee\x84\xba"	// U+e13a
+#define ICON_FA_CODE_FORK "\xee\x84\xbb"	// U+e13b
+#define ICON_FA_CODE_MERGE "\xef\x8e\x87"	// U+f387
+#define ICON_FA_CODE_PULL_REQUEST "\xee\x84\xbc"	// U+e13c
+#define ICON_FA_COINS "\xef\x94\x9e"	// U+f51e
+#define ICON_FA_COLON_SIGN "\xee\x85\x80"	// U+e140
+#define ICON_FA_COMMENT "\xef\x81\xb5"	// U+f075
+#define ICON_FA_COMMENT_DOLLAR "\xef\x99\x91"	// U+f651
+#define ICON_FA_COMMENT_DOTS "\xef\x92\xad"	// U+f4ad
+#define ICON_FA_COMMENT_MEDICAL "\xef\x9f\xb5"	// U+f7f5
+#define ICON_FA_COMMENT_SLASH "\xef\x92\xb3"	// U+f4b3
+#define ICON_FA_COMMENT_SMS "\xef\x9f\x8d"	// U+f7cd
+#define ICON_FA_COMMENTS "\xef\x82\x86"	// U+f086
+#define ICON_FA_COMMENTS_DOLLAR "\xef\x99\x93"	// U+f653
+#define ICON_FA_COMPACT_DISC "\xef\x94\x9f"	// U+f51f
+#define ICON_FA_COMPASS "\xef\x85\x8e"	// U+f14e
+#define ICON_FA_COMPASS_DRAFTING "\xef\x95\xa8"	// U+f568
+#define ICON_FA_COMPRESS "\xef\x81\xa6"	// U+f066
+#define ICON_FA_COMPUTER "\xee\x93\xa5"	// U+e4e5
+#define ICON_FA_COMPUTER_MOUSE "\xef\xa3\x8c"	// U+f8cc
+#define ICON_FA_COOKIE "\xef\x95\xa3"	// U+f563
+#define ICON_FA_COOKIE_BITE "\xef\x95\xa4"	// U+f564
+#define ICON_FA_COPY "\xef\x83\x85"	// U+f0c5
+#define ICON_FA_COPYRIGHT "\xef\x87\xb9"	// U+f1f9
+#define ICON_FA_COUCH "\xef\x92\xb8"	// U+f4b8
+#define ICON_FA_COW "\xef\x9b\x88"	// U+f6c8
+#define ICON_FA_CREDIT_CARD "\xef\x82\x9d"	// U+f09d
+#define ICON_FA_CROP "\xef\x84\xa5"	// U+f125
+#define ICON_FA_CROP_SIMPLE "\xef\x95\xa5"	// U+f565
+#define ICON_FA_CROSS "\xef\x99\x94"	// U+f654
+#define ICON_FA_CROSSHAIRS "\xef\x81\x9b"	// U+f05b
+#define ICON_FA_CROW "\xef\x94\xa0"	// U+f520
+#define ICON_FA_CROWN "\xef\x94\xa1"	// U+f521
+#define ICON_FA_CRUTCH "\xef\x9f\xb7"	// U+f7f7
+#define ICON_FA_CRUZEIRO_SIGN "\xee\x85\x92"	// U+e152
+#define ICON_FA_CUBE "\xef\x86\xb2"	// U+f1b2
+#define ICON_FA_CUBES "\xef\x86\xb3"	// U+f1b3
+#define ICON_FA_CUBES_STACKED "\xee\x93\xa6"	// U+e4e6
+#define ICON_FA_D "D"	// U+44
+#define ICON_FA_DATABASE "\xef\x87\x80"	// U+f1c0
+#define ICON_FA_DELETE_LEFT "\xef\x95\x9a"	// U+f55a
+#define ICON_FA_DEMOCRAT "\xef\x9d\x87"	// U+f747
+#define ICON_FA_DESKTOP "\xef\x8e\x90"	// U+f390
+#define ICON_FA_DHARMACHAKRA "\xef\x99\x95"	// U+f655
+#define ICON_FA_DIAGRAM_NEXT "\xee\x91\xb6"	// U+e476
+#define ICON_FA_DIAGRAM_PREDECESSOR "\xee\x91\xb7"	// U+e477
+#define ICON_FA_DIAGRAM_PROJECT "\xef\x95\x82"	// U+f542
+#define ICON_FA_DIAGRAM_SUCCESSOR "\xee\x91\xba"	// U+e47a
+#define ICON_FA_DIAMOND "\xef\x88\x99"	// U+f219
+#define ICON_FA_DIAMOND_TURN_RIGHT "\xef\x97\xab"	// U+f5eb
+#define ICON_FA_DICE "\xef\x94\xa2"	// U+f522
+#define ICON_FA_DICE_D20 "\xef\x9b\x8f"	// U+f6cf
+#define ICON_FA_DICE_D6 "\xef\x9b\x91"	// U+f6d1
+#define ICON_FA_DICE_FIVE "\xef\x94\xa3"	// U+f523
+#define ICON_FA_DICE_FOUR "\xef\x94\xa4"	// U+f524
+#define ICON_FA_DICE_ONE "\xef\x94\xa5"	// U+f525
+#define ICON_FA_DICE_SIX "\xef\x94\xa6"	// U+f526
+#define ICON_FA_DICE_THREE "\xef\x94\xa7"	// U+f527
+#define ICON_FA_DICE_TWO "\xef\x94\xa8"	// U+f528
+#define ICON_FA_DISEASE "\xef\x9f\xba"	// U+f7fa
+#define ICON_FA_DISPLAY "\xee\x85\xa3"	// U+e163
+#define ICON_FA_DIVIDE "\xef\x94\xa9"	// U+f529
+#define ICON_FA_DNA "\xef\x91\xb1"	// U+f471
+#define ICON_FA_DOG "\xef\x9b\x93"	// U+f6d3
+#define ICON_FA_DOLLAR_SIGN "$"	// U+24
+#define ICON_FA_DOLLY "\xef\x91\xb2"	// U+f472
+#define ICON_FA_DONG_SIGN "\xee\x85\xa9"	// U+e169
+#define ICON_FA_DOOR_CLOSED "\xef\x94\xaa"	// U+f52a
+#define ICON_FA_DOOR_OPEN "\xef\x94\xab"	// U+f52b
+#define ICON_FA_DOVE "\xef\x92\xba"	// U+f4ba
+#define ICON_FA_DOWN_LEFT_AND_UP_RIGHT_TO_CENTER "\xef\x90\xa2"	// U+f422
+#define ICON_FA_DOWN_LONG "\xef\x8c\x89"	// U+f309
+#define ICON_FA_DOWNLOAD "\xef\x80\x99"	// U+f019
+#define ICON_FA_DRAGON "\xef\x9b\x95"	// U+f6d5
+#define ICON_FA_DRAW_POLYGON "\xef\x97\xae"	// U+f5ee
+#define ICON_FA_DROPLET "\xef\x81\x83"	// U+f043
+#define ICON_FA_DROPLET_SLASH "\xef\x97\x87"	// U+f5c7
+#define ICON_FA_DRUM "\xef\x95\xa9"	// U+f569
+#define ICON_FA_DRUM_STEELPAN "\xef\x95\xaa"	// U+f56a
+#define ICON_FA_DRUMSTICK_BITE "\xef\x9b\x97"	// U+f6d7
+#define ICON_FA_DUMBBELL "\xef\x91\x8b"	// U+f44b
+#define ICON_FA_DUMPSTER "\xef\x9e\x93"	// U+f793
+#define ICON_FA_DUMPSTER_FIRE "\xef\x9e\x94"	// U+f794
+#define ICON_FA_DUNGEON "\xef\x9b\x99"	// U+f6d9
+#define ICON_FA_E "E"	// U+45
+#define ICON_FA_EAR_DEAF "\xef\x8a\xa4"	// U+f2a4
+#define ICON_FA_EAR_LISTEN "\xef\x8a\xa2"	// U+f2a2
+#define ICON_FA_EARTH_AFRICA "\xef\x95\xbc"	// U+f57c
+#define ICON_FA_EARTH_AMERICAS "\xef\x95\xbd"	// U+f57d
+#define ICON_FA_EARTH_ASIA "\xef\x95\xbe"	// U+f57e
+#define ICON_FA_EARTH_EUROPE "\xef\x9e\xa2"	// U+f7a2
+#define ICON_FA_EARTH_OCEANIA "\xee\x91\xbb"	// U+e47b
+#define ICON_FA_EGG "\xef\x9f\xbb"	// U+f7fb
+#define ICON_FA_EJECT "\xef\x81\x92"	// U+f052
+#define ICON_FA_ELEVATOR "\xee\x85\xad"	// U+e16d
+#define ICON_FA_ELLIPSIS "\xef\x85\x81"	// U+f141
+#define ICON_FA_ELLIPSIS_VERTICAL "\xef\x85\x82"	// U+f142
+#define ICON_FA_ENVELOPE "\xef\x83\xa0"	// U+f0e0
+#define ICON_FA_ENVELOPE_CIRCLE_CHECK "\xee\x93\xa8"	// U+e4e8
+#define ICON_FA_ENVELOPE_OPEN "\xef\x8a\xb6"	// U+f2b6
+#define ICON_FA_ENVELOPE_OPEN_TEXT "\xef\x99\x98"	// U+f658
+#define ICON_FA_ENVELOPES_BULK "\xef\x99\xb4"	// U+f674
+#define ICON_FA_EQUALS "="	// U+3d
+#define ICON_FA_ERASER "\xef\x84\xad"	// U+f12d
+#define ICON_FA_ETHERNET "\xef\x9e\x96"	// U+f796
+#define ICON_FA_EURO_SIGN "\xef\x85\x93"	// U+f153
+#define ICON_FA_EXCLAMATION "!"	// U+21
+#define ICON_FA_EXPAND "\xef\x81\xa5"	// U+f065
+#define ICON_FA_EXPLOSION "\xee\x93\xa9"	// U+e4e9
+#define ICON_FA_EYE "\xef\x81\xae"	// U+f06e
+#define ICON_FA_EYE_DROPPER "\xef\x87\xbb"	// U+f1fb
+#define ICON_FA_EYE_LOW_VISION "\xef\x8a\xa8"	// U+f2a8
+#define ICON_FA_EYE_SLASH "\xef\x81\xb0"	// U+f070
+#define ICON_FA_F "F"	// U+46
+#define ICON_FA_FACE_ANGRY "\xef\x95\x96"	// U+f556
+#define ICON_FA_FACE_DIZZY "\xef\x95\xa7"	// U+f567
+#define ICON_FA_FACE_FLUSHED "\xef\x95\xb9"	// U+f579
+#define ICON_FA_FACE_FROWN "\xef\x84\x99"	// U+f119
+#define ICON_FA_FACE_FROWN_OPEN "\xef\x95\xba"	// U+f57a
+#define ICON_FA_FACE_GRIMACE "\xef\x95\xbf"	// U+f57f
+#define ICON_FA_FACE_GRIN "\xef\x96\x80"	// U+f580
+#define ICON_FA_FACE_GRIN_BEAM "\xef\x96\x82"	// U+f582
+#define ICON_FA_FACE_GRIN_BEAM_SWEAT "\xef\x96\x83"	// U+f583
+#define ICON_FA_FACE_GRIN_HEARTS "\xef\x96\x84"	// U+f584
+#define ICON_FA_FACE_GRIN_SQUINT "\xef\x96\x85"	// U+f585
+#define ICON_FA_FACE_GRIN_SQUINT_TEARS "\xef\x96\x86"	// U+f586
+#define ICON_FA_FACE_GRIN_STARS "\xef\x96\x87"	// U+f587
+#define ICON_FA_FACE_GRIN_TEARS "\xef\x96\x88"	// U+f588
+#define ICON_FA_FACE_GRIN_TONGUE "\xef\x96\x89"	// U+f589
+#define ICON_FA_FACE_GRIN_TONGUE_SQUINT "\xef\x96\x8a"	// U+f58a
+#define ICON_FA_FACE_GRIN_TONGUE_WINK "\xef\x96\x8b"	// U+f58b
+#define ICON_FA_FACE_GRIN_WIDE "\xef\x96\x81"	// U+f581
+#define ICON_FA_FACE_GRIN_WINK "\xef\x96\x8c"	// U+f58c
+#define ICON_FA_FACE_KISS "\xef\x96\x96"	// U+f596
+#define ICON_FA_FACE_KISS_BEAM "\xef\x96\x97"	// U+f597
+#define ICON_FA_FACE_KISS_WINK_HEART "\xef\x96\x98"	// U+f598
+#define ICON_FA_FACE_LAUGH "\xef\x96\x99"	// U+f599
+#define ICON_FA_FACE_LAUGH_BEAM "\xef\x96\x9a"	// U+f59a
+#define ICON_FA_FACE_LAUGH_SQUINT "\xef\x96\x9b"	// U+f59b
+#define ICON_FA_FACE_LAUGH_WINK "\xef\x96\x9c"	// U+f59c
+#define ICON_FA_FACE_MEH "\xef\x84\x9a"	// U+f11a
+#define ICON_FA_FACE_MEH_BLANK "\xef\x96\xa4"	// U+f5a4
+#define ICON_FA_FACE_ROLLING_EYES "\xef\x96\xa5"	// U+f5a5
+#define ICON_FA_FACE_SAD_CRY "\xef\x96\xb3"	// U+f5b3
+#define ICON_FA_FACE_SAD_TEAR "\xef\x96\xb4"	// U+f5b4
+#define ICON_FA_FACE_SMILE "\xef\x84\x98"	// U+f118
+#define ICON_FA_FACE_SMILE_BEAM "\xef\x96\xb8"	// U+f5b8
+#define ICON_FA_FACE_SMILE_WINK "\xef\x93\x9a"	// U+f4da
+#define ICON_FA_FACE_SURPRISE "\xef\x97\x82"	// U+f5c2
+#define ICON_FA_FACE_TIRED "\xef\x97\x88"	// U+f5c8
+#define ICON_FA_FAN "\xef\xa1\xa3"	// U+f863
+#define ICON_FA_FAUCET "\xee\x80\x85"	// U+e005
+#define ICON_FA_FAUCET_DRIP "\xee\x80\x86"	// U+e006
+#define ICON_FA_FAX "\xef\x86\xac"	// U+f1ac
+#define ICON_FA_FEATHER "\xef\x94\xad"	// U+f52d
+#define ICON_FA_FEATHER_POINTED "\xef\x95\xab"	// U+f56b
+#define ICON_FA_FERRY "\xee\x93\xaa"	// U+e4ea
+#define ICON_FA_FILE "\xef\x85\x9b"	// U+f15b
+#define ICON_FA_FILE_ARROW_DOWN "\xef\x95\xad"	// U+f56d
+#define ICON_FA_FILE_ARROW_UP "\xef\x95\xb4"	// U+f574
+#define ICON_FA_FILE_AUDIO "\xef\x87\x87"	// U+f1c7
+#define ICON_FA_FILE_CIRCLE_CHECK "\xee\x92\x93"	// U+e493
+#define ICON_FA_FILE_CIRCLE_EXCLAMATION "\xee\x93\xab"	// U+e4eb
+#define ICON_FA_FILE_CIRCLE_MINUS "\xee\x93\xad"	// U+e4ed
+#define ICON_FA_FILE_CIRCLE_PLUS "\xee\x93\xae"	// U+e4ee
+#define ICON_FA_FILE_CIRCLE_QUESTION "\xee\x93\xaf"	// U+e4ef
+#define ICON_FA_FILE_CIRCLE_XMARK "\xee\x92\x94"	// U+e494
+#define ICON_FA_FILE_CODE "\xef\x87\x89"	// U+f1c9
+#define ICON_FA_FILE_CONTRACT "\xef\x95\xac"	// U+f56c
+#define ICON_FA_FILE_CSV "\xef\x9b\x9d"	// U+f6dd
+#define ICON_FA_FILE_EXCEL "\xef\x87\x83"	// U+f1c3
+#define ICON_FA_FILE_EXPORT "\xef\x95\xae"	// U+f56e
+#define ICON_FA_FILE_IMAGE "\xef\x87\x85"	// U+f1c5
+#define ICON_FA_FILE_IMPORT "\xef\x95\xaf"	// U+f56f
+#define ICON_FA_FILE_INVOICE "\xef\x95\xb0"	// U+f570
+#define ICON_FA_FILE_INVOICE_DOLLAR "\xef\x95\xb1"	// U+f571
+#define ICON_FA_FILE_LINES "\xef\x85\x9c"	// U+f15c
+#define ICON_FA_FILE_MEDICAL "\xef\x91\xb7"	// U+f477
+#define ICON_FA_FILE_PDF "\xef\x87\x81"	// U+f1c1
+#define ICON_FA_FILE_PEN "\xef\x8c\x9c"	// U+f31c
+#define ICON_FA_FILE_POWERPOINT "\xef\x87\x84"	// U+f1c4
+#define ICON_FA_FILE_PRESCRIPTION "\xef\x95\xb2"	// U+f572
+#define ICON_FA_FILE_SHIELD "\xee\x93\xb0"	// U+e4f0
+#define ICON_FA_FILE_SIGNATURE "\xef\x95\xb3"	// U+f573
+#define ICON_FA_FILE_VIDEO "\xef\x87\x88"	// U+f1c8
+#define ICON_FA_FILE_WAVEFORM "\xef\x91\xb8"	// U+f478
+#define ICON_FA_FILE_WORD "\xef\x87\x82"	// U+f1c2
+#define ICON_FA_FILE_ZIPPER "\xef\x87\x86"	// U+f1c6
+#define ICON_FA_FILL "\xef\x95\xb5"	// U+f575
+#define ICON_FA_FILL_DRIP "\xef\x95\xb6"	// U+f576
+#define ICON_FA_FILM "\xef\x80\x88"	// U+f008
+#define ICON_FA_FILTER "\xef\x82\xb0"	// U+f0b0
+#define ICON_FA_FILTER_CIRCLE_DOLLAR "\xef\x99\xa2"	// U+f662
+#define ICON_FA_FILTER_CIRCLE_XMARK "\xee\x85\xbb"	// U+e17b
+#define ICON_FA_FINGERPRINT "\xef\x95\xb7"	// U+f577
+#define ICON_FA_FIRE "\xef\x81\xad"	// U+f06d
+#define ICON_FA_FIRE_BURNER "\xee\x93\xb1"	// U+e4f1
+#define ICON_FA_FIRE_EXTINGUISHER "\xef\x84\xb4"	// U+f134
+#define ICON_FA_FIRE_FLAME_CURVED "\xef\x9f\xa4"	// U+f7e4
+#define ICON_FA_FIRE_FLAME_SIMPLE "\xef\x91\xaa"	// U+f46a
+#define ICON_FA_FISH "\xef\x95\xb8"	// U+f578
+#define ICON_FA_FISH_FINS "\xee\x93\xb2"	// U+e4f2
+#define ICON_FA_FLAG "\xef\x80\xa4"	// U+f024
+#define ICON_FA_FLAG_CHECKERED "\xef\x84\x9e"	// U+f11e
+#define ICON_FA_FLAG_USA "\xef\x9d\x8d"	// U+f74d
+#define ICON_FA_FLASK "\xef\x83\x83"	// U+f0c3
+#define ICON_FA_FLASK_VIAL "\xee\x93\xb3"	// U+e4f3
+#define ICON_FA_FLOPPY_DISK "\xef\x83\x87"	// U+f0c7
+#define ICON_FA_FLORIN_SIGN "\xee\x86\x84"	// U+e184
+#define ICON_FA_FOLDER "\xef\x81\xbb"	// U+f07b
+#define ICON_FA_FOLDER_CLOSED "\xee\x86\x85"	// U+e185
+#define ICON_FA_FOLDER_MINUS "\xef\x99\x9d"	// U+f65d
+#define ICON_FA_FOLDER_OPEN "\xef\x81\xbc"	// U+f07c
+#define ICON_FA_FOLDER_PLUS "\xef\x99\x9e"	// U+f65e
+#define ICON_FA_FOLDER_TREE "\xef\xa0\x82"	// U+f802
+#define ICON_FA_FONT "\xef\x80\xb1"	// U+f031
+#define ICON_FA_FONT_AWESOME "\xef\x8a\xb4"	// U+f2b4
+#define ICON_FA_FOOTBALL "\xef\x91\x8e"	// U+f44e
+#define ICON_FA_FORWARD "\xef\x81\x8e"	// U+f04e
+#define ICON_FA_FORWARD_FAST "\xef\x81\x90"	// U+f050
+#define ICON_FA_FORWARD_STEP "\xef\x81\x91"	// U+f051
+#define ICON_FA_FRANC_SIGN "\xee\x86\x8f"	// U+e18f
+#define ICON_FA_FROG "\xef\x94\xae"	// U+f52e
+#define ICON_FA_FUTBOL "\xef\x87\xa3"	// U+f1e3
+#define ICON_FA_G "G"	// U+47
+#define ICON_FA_GAMEPAD "\xef\x84\x9b"	// U+f11b
+#define ICON_FA_GAS_PUMP "\xef\x94\xaf"	// U+f52f
+#define ICON_FA_GAUGE "\xef\x98\xa4"	// U+f624
+#define ICON_FA_GAUGE_HIGH "\xef\x98\xa5"	// U+f625
+#define ICON_FA_GAUGE_SIMPLE "\xef\x98\xa9"	// U+f629
+#define ICON_FA_GAUGE_SIMPLE_HIGH "\xef\x98\xaa"	// U+f62a
+#define ICON_FA_GAVEL "\xef\x83\xa3"	// U+f0e3
+#define ICON_FA_GEAR "\xef\x80\x93"	// U+f013
+#define ICON_FA_GEARS "\xef\x82\x85"	// U+f085
+#define ICON_FA_GEM "\xef\x8e\xa5"	// U+f3a5
+#define ICON_FA_GENDERLESS "\xef\x88\xad"	// U+f22d
+#define ICON_FA_GHOST "\xef\x9b\xa2"	// U+f6e2
+#define ICON_FA_GIFT "\xef\x81\xab"	// U+f06b
+#define ICON_FA_GIFTS "\xef\x9e\x9c"	// U+f79c
+#define ICON_FA_GLASS_WATER "\xee\x93\xb4"	// U+e4f4
+#define ICON_FA_GLASS_WATER_DROPLET "\xee\x93\xb5"	// U+e4f5
+#define ICON_FA_GLASSES "\xef\x94\xb0"	// U+f530
+#define ICON_FA_GLOBE "\xef\x82\xac"	// U+f0ac
+#define ICON_FA_GOLF_BALL_TEE "\xef\x91\x90"	// U+f450
+#define ICON_FA_GOPURAM "\xef\x99\xa4"	// U+f664
+#define ICON_FA_GRADUATION_CAP "\xef\x86\x9d"	// U+f19d
+#define ICON_FA_GREATER_THAN ">"	// U+3e
+#define ICON_FA_GREATER_THAN_EQUAL "\xef\x94\xb2"	// U+f532
+#define ICON_FA_GRIP "\xef\x96\x8d"	// U+f58d
+#define ICON_FA_GRIP_LINES "\xef\x9e\xa4"	// U+f7a4
+#define ICON_FA_GRIP_LINES_VERTICAL "\xef\x9e\xa5"	// U+f7a5
+#define ICON_FA_GRIP_VERTICAL "\xef\x96\x8e"	// U+f58e
+#define ICON_FA_GROUP_ARROWS_ROTATE "\xee\x93\xb6"	// U+e4f6
+#define ICON_FA_GUARANI_SIGN "\xee\x86\x9a"	// U+e19a
+#define ICON_FA_GUITAR "\xef\x9e\xa6"	// U+f7a6
+#define ICON_FA_GUN "\xee\x86\x9b"	// U+e19b
+#define ICON_FA_H "H"	// U+48
+#define ICON_FA_HAMMER "\xef\x9b\xa3"	// U+f6e3
+#define ICON_FA_HAMSA "\xef\x99\xa5"	// U+f665
+#define ICON_FA_HAND "\xef\x89\x96"	// U+f256
+#define ICON_FA_HAND_BACK_FIST "\xef\x89\x95"	// U+f255
+#define ICON_FA_HAND_DOTS "\xef\x91\xa1"	// U+f461
+#define ICON_FA_HAND_FIST "\xef\x9b\x9e"	// U+f6de
+#define ICON_FA_HAND_HOLDING "\xef\x92\xbd"	// U+f4bd
+#define ICON_FA_HAND_HOLDING_DOLLAR "\xef\x93\x80"	// U+f4c0
+#define ICON_FA_HAND_HOLDING_DROPLET "\xef\x93\x81"	// U+f4c1
+#define ICON_FA_HAND_HOLDING_HAND "\xee\x93\xb7"	// U+e4f7
+#define ICON_FA_HAND_HOLDING_HEART "\xef\x92\xbe"	// U+f4be
+#define ICON_FA_HAND_HOLDING_MEDICAL "\xee\x81\x9c"	// U+e05c
+#define ICON_FA_HAND_LIZARD "\xef\x89\x98"	// U+f258
+#define ICON_FA_HAND_MIDDLE_FINGER "\xef\xa0\x86"	// U+f806
+#define ICON_FA_HAND_PEACE "\xef\x89\x9b"	// U+f25b
+#define ICON_FA_HAND_POINT_DOWN "\xef\x82\xa7"	// U+f0a7
+#define ICON_FA_HAND_POINT_LEFT "\xef\x82\xa5"	// U+f0a5
+#define ICON_FA_HAND_POINT_RIGHT "\xef\x82\xa4"	// U+f0a4
+#define ICON_FA_HAND_POINT_UP "\xef\x82\xa6"	// U+f0a6
+#define ICON_FA_HAND_POINTER "\xef\x89\x9a"	// U+f25a
+#define ICON_FA_HAND_SCISSORS "\xef\x89\x97"	// U+f257
+#define ICON_FA_HAND_SPARKLES "\xee\x81\x9d"	// U+e05d
+#define ICON_FA_HAND_SPOCK "\xef\x89\x99"	// U+f259
+#define ICON_FA_HANDCUFFS "\xee\x93\xb8"	// U+e4f8
+#define ICON_FA_HANDS "\xef\x8a\xa7"	// U+f2a7
+#define ICON_FA_HANDS_ASL_INTERPRETING "\xef\x8a\xa3"	// U+f2a3
+#define ICON_FA_HANDS_BOUND "\xee\x93\xb9"	// U+e4f9
+#define ICON_FA_HANDS_BUBBLES "\xee\x81\x9e"	// U+e05e
+#define ICON_FA_HANDS_CLAPPING "\xee\x86\xa8"	// U+e1a8
+#define ICON_FA_HANDS_HOLDING "\xef\x93\x82"	// U+f4c2
+#define ICON_FA_HANDS_HOLDING_CHILD "\xee\x93\xba"	// U+e4fa
+#define ICON_FA_HANDS_HOLDING_CIRCLE "\xee\x93\xbb"	// U+e4fb
+#define ICON_FA_HANDS_PRAYING "\xef\x9a\x84"	// U+f684
+#define ICON_FA_HANDSHAKE "\xef\x8a\xb5"	// U+f2b5
+#define ICON_FA_HANDSHAKE_ANGLE "\xef\x93\x84"	// U+f4c4
+#define ICON_FA_HANDSHAKE_SIMPLE "\xef\x93\x86"	// U+f4c6
+#define ICON_FA_HANDSHAKE_SIMPLE_SLASH "\xee\x81\x9f"	// U+e05f
+#define ICON_FA_HANDSHAKE_SLASH "\xee\x81\xa0"	// U+e060
+#define ICON_FA_HANUKIAH "\xef\x9b\xa6"	// U+f6e6
+#define ICON_FA_HARD_DRIVE "\xef\x82\xa0"	// U+f0a0
+#define ICON_FA_HASHTAG "#"	// U+23
+#define ICON_FA_HAT_COWBOY "\xef\xa3\x80"	// U+f8c0
+#define ICON_FA_HAT_COWBOY_SIDE "\xef\xa3\x81"	// U+f8c1
+#define ICON_FA_HAT_WIZARD "\xef\x9b\xa8"	// U+f6e8
+#define ICON_FA_HEAD_SIDE_COUGH "\xee\x81\xa1"	// U+e061
+#define ICON_FA_HEAD_SIDE_COUGH_SLASH "\xee\x81\xa2"	// U+e062
+#define ICON_FA_HEAD_SIDE_MASK "\xee\x81\xa3"	// U+e063
+#define ICON_FA_HEAD_SIDE_VIRUS "\xee\x81\xa4"	// U+e064
+#define ICON_FA_HEADING "\xef\x87\x9c"	// U+f1dc
+#define ICON_FA_HEADPHONES "\xef\x80\xa5"	// U+f025
+#define ICON_FA_HEADPHONES_SIMPLE "\xef\x96\x8f"	// U+f58f
+#define ICON_FA_HEADSET "\xef\x96\x90"	// U+f590
+#define ICON_FA_HEART "\xef\x80\x84"	// U+f004
+#define ICON_FA_HEART_CIRCLE_BOLT "\xee\x93\xbc"	// U+e4fc
+#define ICON_FA_HEART_CIRCLE_CHECK "\xee\x93\xbd"	// U+e4fd
+#define ICON_FA_HEART_CIRCLE_EXCLAMATION "\xee\x93\xbe"	// U+e4fe
+#define ICON_FA_HEART_CIRCLE_MINUS "\xee\x93\xbf"	// U+e4ff
+#define ICON_FA_HEART_CIRCLE_PLUS "\xee\x94\x80"	// U+e500
+#define ICON_FA_HEART_CIRCLE_XMARK "\xee\x94\x81"	// U+e501
+#define ICON_FA_HEART_CRACK "\xef\x9e\xa9"	// U+f7a9
+#define ICON_FA_HEART_PULSE "\xef\x88\x9e"	// U+f21e
+#define ICON_FA_HELICOPTER "\xef\x94\xb3"	// U+f533
+#define ICON_FA_HELICOPTER_SYMBOL "\xee\x94\x82"	// U+e502
+#define ICON_FA_HELMET_SAFETY "\xef\xa0\x87"	// U+f807
+#define ICON_FA_HELMET_UN "\xee\x94\x83"	// U+e503
+#define ICON_FA_HIGHLIGHTER "\xef\x96\x91"	// U+f591
+#define ICON_FA_HILL_AVALANCHE "\xee\x94\x87"	// U+e507
+#define ICON_FA_HILL_ROCKSLIDE "\xee\x94\x88"	// U+e508
+#define ICON_FA_HIPPO "\xef\x9b\xad"	// U+f6ed
+#define ICON_FA_HOCKEY_PUCK "\xef\x91\x93"	// U+f453
+#define ICON_FA_HOLLY_BERRY "\xef\x9e\xaa"	// U+f7aa
+#define ICON_FA_HORSE "\xef\x9b\xb0"	// U+f6f0
+#define ICON_FA_HORSE_HEAD "\xef\x9e\xab"	// U+f7ab
+#define ICON_FA_HOSPITAL "\xef\x83\xb8"	// U+f0f8
+#define ICON_FA_HOSPITAL_USER "\xef\xa0\x8d"	// U+f80d
+#define ICON_FA_HOT_TUB_PERSON "\xef\x96\x93"	// U+f593
+#define ICON_FA_HOTDOG "\xef\xa0\x8f"	// U+f80f
+#define ICON_FA_HOTEL "\xef\x96\x94"	// U+f594
+#define ICON_FA_HOURGLASS "\xef\x89\x94"	// U+f254
+#define ICON_FA_HOURGLASS_EMPTY "\xef\x89\x92"	// U+f252
+#define ICON_FA_HOURGLASS_END "\xef\x89\x93"	// U+f253
+#define ICON_FA_HOURGLASS_START "\xef\x89\x91"	// U+f251
+#define ICON_FA_HOUSE "\xef\x80\x95"	// U+f015
+#define ICON_FA_HOUSE_CHIMNEY "\xee\x8e\xaf"	// U+e3af
+#define ICON_FA_HOUSE_CHIMNEY_CRACK "\xef\x9b\xb1"	// U+f6f1
+#define ICON_FA_HOUSE_CHIMNEY_MEDICAL "\xef\x9f\xb2"	// U+f7f2
+#define ICON_FA_HOUSE_CHIMNEY_USER "\xee\x81\xa5"	// U+e065
+#define ICON_FA_HOUSE_CHIMNEY_WINDOW "\xee\x80\x8d"	// U+e00d
+#define ICON_FA_HOUSE_CIRCLE_CHECK "\xee\x94\x89"	// U+e509
+#define ICON_FA_HOUSE_CIRCLE_EXCLAMATION "\xee\x94\x8a"	// U+e50a
+#define ICON_FA_HOUSE_CIRCLE_XMARK "\xee\x94\x8b"	// U+e50b
+#define ICON_FA_HOUSE_CRACK "\xee\x8e\xb1"	// U+e3b1
+#define ICON_FA_HOUSE_FIRE "\xee\x94\x8c"	// U+e50c
+#define ICON_FA_HOUSE_FLAG "\xee\x94\x8d"	// U+e50d
+#define ICON_FA_HOUSE_FLOOD_WATER "\xee\x94\x8e"	// U+e50e
+#define ICON_FA_HOUSE_FLOOD_WATER_CIRCLE_ARROW_RIGHT "\xee\x94\x8f"	// U+e50f
+#define ICON_FA_HOUSE_LAPTOP "\xee\x81\xa6"	// U+e066
+#define ICON_FA_HOUSE_LOCK "\xee\x94\x90"	// U+e510
+#define ICON_FA_HOUSE_MEDICAL "\xee\x8e\xb2"	// U+e3b2
+#define ICON_FA_HOUSE_MEDICAL_CIRCLE_CHECK "\xee\x94\x91"	// U+e511
+#define ICON_FA_HOUSE_MEDICAL_CIRCLE_EXCLAMATION "\xee\x94\x92"	// U+e512
+#define ICON_FA_HOUSE_MEDICAL_CIRCLE_XMARK "\xee\x94\x93"	// U+e513
+#define ICON_FA_HOUSE_MEDICAL_FLAG "\xee\x94\x94"	// U+e514
+#define ICON_FA_HOUSE_SIGNAL "\xee\x80\x92"	// U+e012
+#define ICON_FA_HOUSE_TSUNAMI "\xee\x94\x95"	// U+e515
+#define ICON_FA_HOUSE_USER "\xee\x86\xb0"	// U+e1b0
+#define ICON_FA_HRYVNIA_SIGN "\xef\x9b\xb2"	// U+f6f2
+#define ICON_FA_HURRICANE "\xef\x9d\x91"	// U+f751
+#define ICON_FA_I "I"	// U+49
+#define ICON_FA_I_CURSOR "\xef\x89\x86"	// U+f246
+#define ICON_FA_ICE_CREAM "\xef\xa0\x90"	// U+f810
+#define ICON_FA_ICICLES "\xef\x9e\xad"	// U+f7ad
+#define ICON_FA_ICONS "\xef\xa1\xad"	// U+f86d
+#define ICON_FA_ID_BADGE "\xef\x8b\x81"	// U+f2c1
+#define ICON_FA_ID_CARD "\xef\x8b\x82"	// U+f2c2
+#define ICON_FA_ID_CARD_CLIP "\xef\x91\xbf"	// U+f47f
+#define ICON_FA_IGLOO "\xef\x9e\xae"	// U+f7ae
+#define ICON_FA_IMAGE "\xef\x80\xbe"	// U+f03e
+#define ICON_FA_IMAGE_PORTRAIT "\xef\x8f\xa0"	// U+f3e0
+#define ICON_FA_IMAGES "\xef\x8c\x82"	// U+f302
+#define ICON_FA_INBOX "\xef\x80\x9c"	// U+f01c
+#define ICON_FA_INDENT "\xef\x80\xbc"	// U+f03c
+#define ICON_FA_INDIAN_RUPEE_SIGN "\xee\x86\xbc"	// U+e1bc
+#define ICON_FA_INDUSTRY "\xef\x89\xb5"	// U+f275
+#define ICON_FA_INFINITY "\xef\x94\xb4"	// U+f534
+#define ICON_FA_INFO "\xef\x84\xa9"	// U+f129
+#define ICON_FA_ITALIC "\xef\x80\xb3"	// U+f033
+#define ICON_FA_J "J"	// U+4a
+#define ICON_FA_JAR "\xee\x94\x96"	// U+e516
+#define ICON_FA_JAR_WHEAT "\xee\x94\x97"	// U+e517
+#define ICON_FA_JEDI "\xef\x99\xa9"	// U+f669
+#define ICON_FA_JET_FIGHTER "\xef\x83\xbb"	// U+f0fb
+#define ICON_FA_JET_FIGHTER_UP "\xee\x94\x98"	// U+e518
+#define ICON_FA_JOINT "\xef\x96\x95"	// U+f595
+#define ICON_FA_JUG_DETERGENT "\xee\x94\x99"	// U+e519
+#define ICON_FA_K "K"	// U+4b
+#define ICON_FA_KAABA "\xef\x99\xab"	// U+f66b
+#define ICON_FA_KEY "\xef\x82\x84"	// U+f084
+#define ICON_FA_KEYBOARD "\xef\x84\x9c"	// U+f11c
+#define ICON_FA_KHANDA "\xef\x99\xad"	// U+f66d
+#define ICON_FA_KIP_SIGN "\xee\x87\x84"	// U+e1c4
+#define ICON_FA_KIT_MEDICAL "\xef\x91\xb9"	// U+f479
+#define ICON_FA_KITCHEN_SET "\xee\x94\x9a"	// U+e51a
+#define ICON_FA_KIWI_BIRD "\xef\x94\xb5"	// U+f535
+#define ICON_FA_L "L"	// U+4c
+#define ICON_FA_LAND_MINE_ON "\xee\x94\x9b"	// U+e51b
+#define ICON_FA_LANDMARK "\xef\x99\xaf"	// U+f66f
+#define ICON_FA_LANDMARK_DOME "\xef\x9d\x92"	// U+f752
+#define ICON_FA_LANDMARK_FLAG "\xee\x94\x9c"	// U+e51c
+#define ICON_FA_LANGUAGE "\xef\x86\xab"	// U+f1ab
+#define ICON_FA_LAPTOP "\xef\x84\x89"	// U+f109
+#define ICON_FA_LAPTOP_CODE "\xef\x97\xbc"	// U+f5fc
+#define ICON_FA_LAPTOP_FILE "\xee\x94\x9d"	// U+e51d
+#define ICON_FA_LAPTOP_MEDICAL "\xef\xa0\x92"	// U+f812
+#define ICON_FA_LARI_SIGN "\xee\x87\x88"	// U+e1c8
+#define ICON_FA_LAYER_GROUP "\xef\x97\xbd"	// U+f5fd
+#define ICON_FA_LEAF "\xef\x81\xac"	// U+f06c
+#define ICON_FA_LEFT_LONG "\xef\x8c\x8a"	// U+f30a
+#define ICON_FA_LEFT_RIGHT "\xef\x8c\xb7"	// U+f337
+#define ICON_FA_LEMON "\xef\x82\x94"	// U+f094
+#define ICON_FA_LESS_THAN "<"	// U+3c
+#define ICON_FA_LESS_THAN_EQUAL "\xef\x94\xb7"	// U+f537
+#define ICON_FA_LIFE_RING "\xef\x87\x8d"	// U+f1cd
+#define ICON_FA_LIGHTBULB "\xef\x83\xab"	// U+f0eb
+#define ICON_FA_LINES_LEANING "\xee\x94\x9e"	// U+e51e
+#define ICON_FA_LINK "\xef\x83\x81"	// U+f0c1
+#define ICON_FA_LINK_SLASH "\xef\x84\xa7"	// U+f127
+#define ICON_FA_LIRA_SIGN "\xef\x86\x95"	// U+f195
+#define ICON_FA_LIST "\xef\x80\xba"	// U+f03a
+#define ICON_FA_LIST_CHECK "\xef\x82\xae"	// U+f0ae
+#define ICON_FA_LIST_OL "\xef\x83\x8b"	// U+f0cb
+#define ICON_FA_LIST_UL "\xef\x83\x8a"	// U+f0ca
+#define ICON_FA_LITECOIN_SIGN "\xee\x87\x93"	// U+e1d3
+#define ICON_FA_LOCATION_ARROW "\xef\x84\xa4"	// U+f124
+#define ICON_FA_LOCATION_CROSSHAIRS "\xef\x98\x81"	// U+f601
+#define ICON_FA_LOCATION_DOT "\xef\x8f\x85"	// U+f3c5
+#define ICON_FA_LOCATION_PIN "\xef\x81\x81"	// U+f041
+#define ICON_FA_LOCATION_PIN_LOCK "\xee\x94\x9f"	// U+e51f
+#define ICON_FA_LOCK "\xef\x80\xa3"	// U+f023
+#define ICON_FA_LOCK_OPEN "\xef\x8f\x81"	// U+f3c1
+#define ICON_FA_LOCUST "\xee\x94\xa0"	// U+e520
+#define ICON_FA_LUNGS "\xef\x98\x84"	// U+f604
+#define ICON_FA_LUNGS_VIRUS "\xee\x81\xa7"	// U+e067
+#define ICON_FA_M "M"	// U+4d
+#define ICON_FA_MAGNET "\xef\x81\xb6"	// U+f076
+#define ICON_FA_MAGNIFYING_GLASS "\xef\x80\x82"	// U+f002
+#define ICON_FA_MAGNIFYING_GLASS_ARROW_RIGHT "\xee\x94\xa1"	// U+e521
+#define ICON_FA_MAGNIFYING_GLASS_CHART "\xee\x94\xa2"	// U+e522
+#define ICON_FA_MAGNIFYING_GLASS_DOLLAR "\xef\x9a\x88"	// U+f688
+#define ICON_FA_MAGNIFYING_GLASS_LOCATION "\xef\x9a\x89"	// U+f689
+#define ICON_FA_MAGNIFYING_GLASS_MINUS "\xef\x80\x90"	// U+f010
+#define ICON_FA_MAGNIFYING_GLASS_PLUS "\xef\x80\x8e"	// U+f00e
+#define ICON_FA_MANAT_SIGN "\xee\x87\x95"	// U+e1d5
+#define ICON_FA_MAP "\xef\x89\xb9"	// U+f279
+#define ICON_FA_MAP_LOCATION "\xef\x96\x9f"	// U+f59f
+#define ICON_FA_MAP_LOCATION_DOT "\xef\x96\xa0"	// U+f5a0
+#define ICON_FA_MAP_PIN "\xef\x89\xb6"	// U+f276
+#define ICON_FA_MARKER "\xef\x96\xa1"	// U+f5a1
+#define ICON_FA_MARS "\xef\x88\xa2"	// U+f222
+#define ICON_FA_MARS_AND_VENUS "\xef\x88\xa4"	// U+f224
+#define ICON_FA_MARS_AND_VENUS_BURST "\xee\x94\xa3"	// U+e523
+#define ICON_FA_MARS_DOUBLE "\xef\x88\xa7"	// U+f227
+#define ICON_FA_MARS_STROKE "\xef\x88\xa9"	// U+f229
+#define ICON_FA_MARS_STROKE_RIGHT "\xef\x88\xab"	// U+f22b
+#define ICON_FA_MARS_STROKE_UP "\xef\x88\xaa"	// U+f22a
+#define ICON_FA_MARTINI_GLASS "\xef\x95\xbb"	// U+f57b
+#define ICON_FA_MARTINI_GLASS_CITRUS "\xef\x95\xa1"	// U+f561
+#define ICON_FA_MARTINI_GLASS_EMPTY "\xef\x80\x80"	// U+f000
+#define ICON_FA_MASK "\xef\x9b\xba"	// U+f6fa
+#define ICON_FA_MASK_FACE "\xee\x87\x97"	// U+e1d7
+#define ICON_FA_MASK_VENTILATOR "\xee\x94\xa4"	// U+e524
+#define ICON_FA_MASKS_THEATER "\xef\x98\xb0"	// U+f630
+#define ICON_FA_MATTRESS_PILLOW "\xee\x94\xa5"	// U+e525
+#define ICON_FA_MAXIMIZE "\xef\x8c\x9e"	// U+f31e
+#define ICON_FA_MEDAL "\xef\x96\xa2"	// U+f5a2
+#define ICON_FA_MEMORY "\xef\x94\xb8"	// U+f538
+#define ICON_FA_MENORAH "\xef\x99\xb6"	// U+f676
+#define ICON_FA_MERCURY "\xef\x88\xa3"	// U+f223
+#define ICON_FA_MESSAGE "\xef\x89\xba"	// U+f27a
+#define ICON_FA_METEOR "\xef\x9d\x93"	// U+f753
+#define ICON_FA_MICROCHIP "\xef\x8b\x9b"	// U+f2db
+#define ICON_FA_MICROPHONE "\xef\x84\xb0"	// U+f130
+#define ICON_FA_MICROPHONE_LINES "\xef\x8f\x89"	// U+f3c9
+#define ICON_FA_MICROPHONE_LINES_SLASH "\xef\x94\xb9"	// U+f539
+#define ICON_FA_MICROPHONE_SLASH "\xef\x84\xb1"	// U+f131
+#define ICON_FA_MICROSCOPE "\xef\x98\x90"	// U+f610
+#define ICON_FA_MILL_SIGN "\xee\x87\xad"	// U+e1ed
+#define ICON_FA_MINIMIZE "\xef\x9e\x8c"	// U+f78c
+#define ICON_FA_MINUS "\xef\x81\xa8"	// U+f068
+#define ICON_FA_MITTEN "\xef\x9e\xb5"	// U+f7b5
+#define ICON_FA_MOBILE "\xef\x8f\x8e"	// U+f3ce
+#define ICON_FA_MOBILE_BUTTON "\xef\x84\x8b"	// U+f10b
+#define ICON_FA_MOBILE_RETRO "\xee\x94\xa7"	// U+e527
+#define ICON_FA_MOBILE_SCREEN "\xef\x8f\x8f"	// U+f3cf
+#define ICON_FA_MOBILE_SCREEN_BUTTON "\xef\x8f\x8d"	// U+f3cd
+#define ICON_FA_MONEY_BILL "\xef\x83\x96"	// U+f0d6
+#define ICON_FA_MONEY_BILL_1 "\xef\x8f\x91"	// U+f3d1
+#define ICON_FA_MONEY_BILL_1_WAVE "\xef\x94\xbb"	// U+f53b
+#define ICON_FA_MONEY_BILL_TRANSFER "\xee\x94\xa8"	// U+e528
+#define ICON_FA_MONEY_BILL_TREND_UP "\xee\x94\xa9"	// U+e529
+#define ICON_FA_MONEY_BILL_WAVE "\xef\x94\xba"	// U+f53a
+#define ICON_FA_MONEY_BILL_WHEAT "\xee\x94\xaa"	// U+e52a
+#define ICON_FA_MONEY_BILLS "\xee\x87\xb3"	// U+e1f3
+#define ICON_FA_MONEY_CHECK "\xef\x94\xbc"	// U+f53c
+#define ICON_FA_MONEY_CHECK_DOLLAR "\xef\x94\xbd"	// U+f53d
+#define ICON_FA_MONUMENT "\xef\x96\xa6"	// U+f5a6
+#define ICON_FA_MOON "\xef\x86\x86"	// U+f186
+#define ICON_FA_MORTAR_PESTLE "\xef\x96\xa7"	// U+f5a7
+#define ICON_FA_MOSQUE "\xef\x99\xb8"	// U+f678
+#define ICON_FA_MOSQUITO "\xee\x94\xab"	// U+e52b
+#define ICON_FA_MOSQUITO_NET "\xee\x94\xac"	// U+e52c
+#define ICON_FA_MOTORCYCLE "\xef\x88\x9c"	// U+f21c
+#define ICON_FA_MOUND "\xee\x94\xad"	// U+e52d
+#define ICON_FA_MOUNTAIN "\xef\x9b\xbc"	// U+f6fc
+#define ICON_FA_MOUNTAIN_CITY "\xee\x94\xae"	// U+e52e
+#define ICON_FA_MOUNTAIN_SUN "\xee\x94\xaf"	// U+e52f
+#define ICON_FA_MUG_HOT "\xef\x9e\xb6"	// U+f7b6
+#define ICON_FA_MUG_SAUCER "\xef\x83\xb4"	// U+f0f4
+#define ICON_FA_MUSIC "\xef\x80\x81"	// U+f001
+#define ICON_FA_N "N"	// U+4e
+#define ICON_FA_NAIRA_SIGN "\xee\x87\xb6"	// U+e1f6
+#define ICON_FA_NETWORK_WIRED "\xef\x9b\xbf"	// U+f6ff
+#define ICON_FA_NEUTER "\xef\x88\xac"	// U+f22c
+#define ICON_FA_NEWSPAPER "\xef\x87\xaa"	// U+f1ea
+#define ICON_FA_NOT_EQUAL "\xef\x94\xbe"	// U+f53e
+#define ICON_FA_NOTE_STICKY "\xef\x89\x89"	// U+f249
+#define ICON_FA_NOTES_MEDICAL "\xef\x92\x81"	// U+f481
+#define ICON_FA_O "O"	// U+4f
+#define ICON_FA_OBJECT_GROUP "\xef\x89\x87"	// U+f247
+#define ICON_FA_OBJECT_UNGROUP "\xef\x89\x88"	// U+f248
+#define ICON_FA_OIL_CAN "\xef\x98\x93"	// U+f613
+#define ICON_FA_OIL_WELL "\xee\x94\xb2"	// U+e532
+#define ICON_FA_OM "\xef\x99\xb9"	// U+f679
+#define ICON_FA_OTTER "\xef\x9c\x80"	// U+f700
+#define ICON_FA_OUTDENT "\xef\x80\xbb"	// U+f03b
+#define ICON_FA_P "P"	// U+50
+#define ICON_FA_PAGER "\xef\xa0\x95"	// U+f815
+#define ICON_FA_PAINT_ROLLER "\xef\x96\xaa"	// U+f5aa
+#define ICON_FA_PAINTBRUSH "\xef\x87\xbc"	// U+f1fc
+#define ICON_FA_PALETTE "\xef\x94\xbf"	// U+f53f
+#define ICON_FA_PALLET "\xef\x92\x82"	// U+f482
+#define ICON_FA_PANORAMA "\xee\x88\x89"	// U+e209
+#define ICON_FA_PAPER_PLANE "\xef\x87\x98"	// U+f1d8
+#define ICON_FA_PAPERCLIP "\xef\x83\x86"	// U+f0c6
+#define ICON_FA_PARACHUTE_BOX "\xef\x93\x8d"	// U+f4cd
+#define ICON_FA_PARAGRAPH "\xef\x87\x9d"	// U+f1dd
+#define ICON_FA_PASSPORT "\xef\x96\xab"	// U+f5ab
+#define ICON_FA_PASTE "\xef\x83\xaa"	// U+f0ea
+#define ICON_FA_PAUSE "\xef\x81\x8c"	// U+f04c
+#define ICON_FA_PAW "\xef\x86\xb0"	// U+f1b0
+#define ICON_FA_PEACE "\xef\x99\xbc"	// U+f67c
+#define ICON_FA_PEN "\xef\x8c\x84"	// U+f304
+#define ICON_FA_PEN_CLIP "\xef\x8c\x85"	// U+f305
+#define ICON_FA_PEN_FANCY "\xef\x96\xac"	// U+f5ac
+#define ICON_FA_PEN_NIB "\xef\x96\xad"	// U+f5ad
+#define ICON_FA_PEN_RULER "\xef\x96\xae"	// U+f5ae
+#define ICON_FA_PEN_TO_SQUARE "\xef\x81\x84"	// U+f044
+#define ICON_FA_PENCIL "\xef\x8c\x83"	// U+f303
+#define ICON_FA_PEOPLE_ARROWS_LEFT_RIGHT "\xee\x81\xa8"	// U+e068
+#define ICON_FA_PEOPLE_CARRY_BOX "\xef\x93\x8e"	// U+f4ce
+#define ICON_FA_PEOPLE_GROUP "\xee\x94\xb3"	// U+e533
+#define ICON_FA_PEOPLE_LINE "\xee\x94\xb4"	// U+e534
+#define ICON_FA_PEOPLE_PULLING "\xee\x94\xb5"	// U+e535
+#define ICON_FA_PEOPLE_ROBBERY "\xee\x94\xb6"	// U+e536
+#define ICON_FA_PEOPLE_ROOF "\xee\x94\xb7"	// U+e537
+#define ICON_FA_PEPPER_HOT "\xef\xa0\x96"	// U+f816
+#define ICON_FA_PERCENT "%"	// U+25
+#define ICON_FA_PERSON "\xef\x86\x83"	// U+f183
+#define ICON_FA_PERSON_ARROW_DOWN_TO_LINE "\xee\x94\xb8"	// U+e538
+#define ICON_FA_PERSON_ARROW_UP_FROM_LINE "\xee\x94\xb9"	// U+e539
+#define ICON_FA_PERSON_BIKING "\xef\xa1\x8a"	// U+f84a
+#define ICON_FA_PERSON_BOOTH "\xef\x9d\x96"	// U+f756
+#define ICON_FA_PERSON_BREASTFEEDING "\xee\x94\xba"	// U+e53a
+#define ICON_FA_PERSON_BURST "\xee\x94\xbb"	// U+e53b
+#define ICON_FA_PERSON_CANE "\xee\x94\xbc"	// U+e53c
+#define ICON_FA_PERSON_CHALKBOARD "\xee\x94\xbd"	// U+e53d
+#define ICON_FA_PERSON_CIRCLE_CHECK "\xee\x94\xbe"	// U+e53e
+#define ICON_FA_PERSON_CIRCLE_EXCLAMATION "\xee\x94\xbf"	// U+e53f
+#define ICON_FA_PERSON_CIRCLE_MINUS "\xee\x95\x80"	// U+e540
+#define ICON_FA_PERSON_CIRCLE_PLUS "\xee\x95\x81"	// U+e541
+#define ICON_FA_PERSON_CIRCLE_QUESTION "\xee\x95\x82"	// U+e542
+#define ICON_FA_PERSON_CIRCLE_XMARK "\xee\x95\x83"	// U+e543
+#define ICON_FA_PERSON_DIGGING "\xef\xa1\x9e"	// U+f85e
+#define ICON_FA_PERSON_DOTS_FROM_LINE "\xef\x91\xb0"	// U+f470
+#define ICON_FA_PERSON_DRESS "\xef\x86\x82"	// U+f182
+#define ICON_FA_PERSON_DRESS_BURST "\xee\x95\x84"	// U+e544
+#define ICON_FA_PERSON_DROWNING "\xee\x95\x85"	// U+e545
+#define ICON_FA_PERSON_FALLING "\xee\x95\x86"	// U+e546
+#define ICON_FA_PERSON_FALLING_BURST "\xee\x95\x87"	// U+e547
+#define ICON_FA_PERSON_HALF_DRESS "\xee\x95\x88"	// U+e548
+#define ICON_FA_PERSON_HARASSING "\xee\x95\x89"	// U+e549
+#define ICON_FA_PERSON_HIKING "\xef\x9b\xac"	// U+f6ec
+#define ICON_FA_PERSON_MILITARY_POINTING "\xee\x95\x8a"	// U+e54a
+#define ICON_FA_PERSON_MILITARY_RIFLE "\xee\x95\x8b"	// U+e54b
+#define ICON_FA_PERSON_MILITARY_TO_PERSON "\xee\x95\x8c"	// U+e54c
+#define ICON_FA_PERSON_PRAYING "\xef\x9a\x83"	// U+f683
+#define ICON_FA_PERSON_PREGNANT "\xee\x8c\x9e"	// U+e31e
+#define ICON_FA_PERSON_RAYS "\xee\x95\x8d"	// U+e54d
+#define ICON_FA_PERSON_RIFLE "\xee\x95\x8e"	// U+e54e
+#define ICON_FA_PERSON_RUNNING "\xef\x9c\x8c"	// U+f70c
+#define ICON_FA_PERSON_SHELTER "\xee\x95\x8f"	// U+e54f
+#define ICON_FA_PERSON_SKATING "\xef\x9f\x85"	// U+f7c5
+#define ICON_FA_PERSON_SKIING "\xef\x9f\x89"	// U+f7c9
+#define ICON_FA_PERSON_SKIING_NORDIC "\xef\x9f\x8a"	// U+f7ca
+#define ICON_FA_PERSON_SNOWBOARDING "\xef\x9f\x8e"	// U+f7ce
+#define ICON_FA_PERSON_SWIMMING "\xef\x97\x84"	// U+f5c4
+#define ICON_FA_PERSON_THROUGH_WINDOW "\xee\x90\xb3"	// U+e433
+#define ICON_FA_PERSON_WALKING "\xef\x95\x94"	// U+f554
+#define ICON_FA_PERSON_WALKING_ARROW_LOOP_LEFT "\xee\x95\x91"	// U+e551
+#define ICON_FA_PERSON_WALKING_ARROW_RIGHT "\xee\x95\x92"	// U+e552
+#define ICON_FA_PERSON_WALKING_DASHED_LINE_ARROW_RIGHT "\xee\x95\x93"	// U+e553
+#define ICON_FA_PERSON_WALKING_LUGGAGE "\xee\x95\x94"	// U+e554
+#define ICON_FA_PERSON_WALKING_WITH_CANE "\xef\x8a\x9d"	// U+f29d
+#define ICON_FA_PESETA_SIGN "\xee\x88\xa1"	// U+e221
+#define ICON_FA_PESO_SIGN "\xee\x88\xa2"	// U+e222
+#define ICON_FA_PHONE "\xef\x82\x95"	// U+f095
+#define ICON_FA_PHONE_FLIP "\xef\xa1\xb9"	// U+f879
+#define ICON_FA_PHONE_SLASH "\xef\x8f\x9d"	// U+f3dd
+#define ICON_FA_PHONE_VOLUME "\xef\x8a\xa0"	// U+f2a0
+#define ICON_FA_PHOTO_FILM "\xef\xa1\xbc"	// U+f87c
+#define ICON_FA_PIGGY_BANK "\xef\x93\x93"	// U+f4d3
+#define ICON_FA_PILLS "\xef\x92\x84"	// U+f484
+#define ICON_FA_PIZZA_SLICE "\xef\xa0\x98"	// U+f818
+#define ICON_FA_PLACE_OF_WORSHIP "\xef\x99\xbf"	// U+f67f
+#define ICON_FA_PLANE "\xef\x81\xb2"	// U+f072
+#define ICON_FA_PLANE_ARRIVAL "\xef\x96\xaf"	// U+f5af
+#define ICON_FA_PLANE_CIRCLE_CHECK "\xee\x95\x95"	// U+e555
+#define ICON_FA_PLANE_CIRCLE_EXCLAMATION "\xee\x95\x96"	// U+e556
+#define ICON_FA_PLANE_CIRCLE_XMARK "\xee\x95\x97"	// U+e557
+#define ICON_FA_PLANE_DEPARTURE "\xef\x96\xb0"	// U+f5b0
+#define ICON_FA_PLANE_LOCK "\xee\x95\x98"	// U+e558
+#define ICON_FA_PLANE_SLASH "\xee\x81\xa9"	// U+e069
+#define ICON_FA_PLANE_UP "\xee\x88\xad"	// U+e22d
+#define ICON_FA_PLANT_WILT "\xee\x90\xbb"	// U+e43b
+#define ICON_FA_PLATE_WHEAT "\xee\x95\x9a"	// U+e55a
+#define ICON_FA_PLAY "\xef\x81\x8b"	// U+f04b
+#define ICON_FA_PLUG "\xef\x87\xa6"	// U+f1e6
+#define ICON_FA_PLUG_CIRCLE_BOLT "\xee\x95\x9b"	// U+e55b
+#define ICON_FA_PLUG_CIRCLE_CHECK "\xee\x95\x9c"	// U+e55c
+#define ICON_FA_PLUG_CIRCLE_EXCLAMATION "\xee\x95\x9d"	// U+e55d
+#define ICON_FA_PLUG_CIRCLE_MINUS "\xee\x95\x9e"	// U+e55e
+#define ICON_FA_PLUG_CIRCLE_PLUS "\xee\x95\x9f"	// U+e55f
+#define ICON_FA_PLUG_CIRCLE_XMARK "\xee\x95\xa0"	// U+e560
+#define ICON_FA_PLUS "+"	// U+2b
+#define ICON_FA_PLUS_MINUS "\xee\x90\xbc"	// U+e43c
+#define ICON_FA_PODCAST "\xef\x8b\x8e"	// U+f2ce
+#define ICON_FA_POO "\xef\x8b\xbe"	// U+f2fe
+#define ICON_FA_POO_STORM "\xef\x9d\x9a"	// U+f75a
+#define ICON_FA_POOP "\xef\x98\x99"	// U+f619
+#define ICON_FA_POWER_OFF "\xef\x80\x91"	// U+f011
+#define ICON_FA_PRESCRIPTION "\xef\x96\xb1"	// U+f5b1
+#define ICON_FA_PRESCRIPTION_BOTTLE "\xef\x92\x85"	// U+f485
+#define ICON_FA_PRESCRIPTION_BOTTLE_MEDICAL "\xef\x92\x86"	// U+f486
+#define ICON_FA_PRINT "\xef\x80\xaf"	// U+f02f
+#define ICON_FA_PUMP_MEDICAL "\xee\x81\xaa"	// U+e06a
+#define ICON_FA_PUMP_SOAP "\xee\x81\xab"	// U+e06b
+#define ICON_FA_PUZZLE_PIECE "\xef\x84\xae"	// U+f12e
+#define ICON_FA_Q "Q"	// U+51
+#define ICON_FA_QRCODE "\xef\x80\xa9"	// U+f029
+#define ICON_FA_QUESTION "?"	// U+3f
+#define ICON_FA_QUOTE_LEFT "\xef\x84\x8d"	// U+f10d
+#define ICON_FA_QUOTE_RIGHT "\xef\x84\x8e"	// U+f10e
+#define ICON_FA_R "R"	// U+52
+#define ICON_FA_RADIATION "\xef\x9e\xb9"	// U+f7b9
+#define ICON_FA_RADIO "\xef\xa3\x97"	// U+f8d7
+#define ICON_FA_RAINBOW "\xef\x9d\x9b"	// U+f75b
+#define ICON_FA_RANKING_STAR "\xee\x95\xa1"	// U+e561
+#define ICON_FA_RECEIPT "\xef\x95\x83"	// U+f543
+#define ICON_FA_RECORD_VINYL "\xef\xa3\x99"	// U+f8d9
+#define ICON_FA_RECTANGLE_AD "\xef\x99\x81"	// U+f641
+#define ICON_FA_RECTANGLE_LIST "\xef\x80\xa2"	// U+f022
+#define ICON_FA_RECTANGLE_XMARK "\xef\x90\x90"	// U+f410
+#define ICON_FA_RECYCLE "\xef\x86\xb8"	// U+f1b8
+#define ICON_FA_REGISTERED "\xef\x89\x9d"	// U+f25d
+#define ICON_FA_REPEAT "\xef\x8d\xa3"	// U+f363
+#define ICON_FA_REPLY "\xef\x8f\xa5"	// U+f3e5
+#define ICON_FA_REPLY_ALL "\xef\x84\xa2"	// U+f122
+#define ICON_FA_REPUBLICAN "\xef\x9d\x9e"	// U+f75e
+#define ICON_FA_RESTROOM "\xef\x9e\xbd"	// U+f7bd
+#define ICON_FA_RETWEET "\xef\x81\xb9"	// U+f079
+#define ICON_FA_RIBBON "\xef\x93\x96"	// U+f4d6
+#define ICON_FA_RIGHT_FROM_BRACKET "\xef\x8b\xb5"	// U+f2f5
+#define ICON_FA_RIGHT_LEFT "\xef\x8d\xa2"	// U+f362
+#define ICON_FA_RIGHT_LONG "\xef\x8c\x8b"	// U+f30b
+#define ICON_FA_RIGHT_TO_BRACKET "\xef\x8b\xb6"	// U+f2f6
+#define ICON_FA_RING "\xef\x9c\x8b"	// U+f70b
+#define ICON_FA_ROAD "\xef\x80\x98"	// U+f018
+#define ICON_FA_ROAD_BARRIER "\xee\x95\xa2"	// U+e562
+#define ICON_FA_ROAD_BRIDGE "\xee\x95\xa3"	// U+e563
+#define ICON_FA_ROAD_CIRCLE_CHECK "\xee\x95\xa4"	// U+e564
+#define ICON_FA_ROAD_CIRCLE_EXCLAMATION "\xee\x95\xa5"	// U+e565
+#define ICON_FA_ROAD_CIRCLE_XMARK "\xee\x95\xa6"	// U+e566
+#define ICON_FA_ROAD_LOCK "\xee\x95\xa7"	// U+e567
+#define ICON_FA_ROAD_SPIKES "\xee\x95\xa8"	// U+e568
+#define ICON_FA_ROBOT "\xef\x95\x84"	// U+f544
+#define ICON_FA_ROCKET "\xef\x84\xb5"	// U+f135
+#define ICON_FA_ROTATE "\xef\x8b\xb1"	// U+f2f1
+#define ICON_FA_ROTATE_LEFT "\xef\x8b\xaa"	// U+f2ea
+#define ICON_FA_ROTATE_RIGHT "\xef\x8b\xb9"	// U+f2f9
+#define ICON_FA_ROUTE "\xef\x93\x97"	// U+f4d7
+#define ICON_FA_RSS "\xef\x82\x9e"	// U+f09e
+#define ICON_FA_RUBLE_SIGN "\xef\x85\x98"	// U+f158
+#define ICON_FA_RUG "\xee\x95\xa9"	// U+e569
+#define ICON_FA_RULER "\xef\x95\x85"	// U+f545
+#define ICON_FA_RULER_COMBINED "\xef\x95\x86"	// U+f546
+#define ICON_FA_RULER_HORIZONTAL "\xef\x95\x87"	// U+f547
+#define ICON_FA_RULER_VERTICAL "\xef\x95\x88"	// U+f548
+#define ICON_FA_RUPEE_SIGN "\xef\x85\x96"	// U+f156
+#define ICON_FA_RUPIAH_SIGN "\xee\x88\xbd"	// U+e23d
+#define ICON_FA_S "S"	// U+53
+#define ICON_FA_SACK_DOLLAR "\xef\xa0\x9d"	// U+f81d
+#define ICON_FA_SACK_XMARK "\xee\x95\xaa"	// U+e56a
+#define ICON_FA_SAILBOAT "\xee\x91\x85"	// U+e445
+#define ICON_FA_SATELLITE "\xef\x9e\xbf"	// U+f7bf
+#define ICON_FA_SATELLITE_DISH "\xef\x9f\x80"	// U+f7c0
+#define ICON_FA_SCALE_BALANCED "\xef\x89\x8e"	// U+f24e
+#define ICON_FA_SCALE_UNBALANCED "\xef\x94\x95"	// U+f515
+#define ICON_FA_SCALE_UNBALANCED_FLIP "\xef\x94\x96"	// U+f516
+#define ICON_FA_SCHOOL "\xef\x95\x89"	// U+f549
+#define ICON_FA_SCHOOL_CIRCLE_CHECK "\xee\x95\xab"	// U+e56b
+#define ICON_FA_SCHOOL_CIRCLE_EXCLAMATION "\xee\x95\xac"	// U+e56c
+#define ICON_FA_SCHOOL_CIRCLE_XMARK "\xee\x95\xad"	// U+e56d
+#define ICON_FA_SCHOOL_FLAG "\xee\x95\xae"	// U+e56e
+#define ICON_FA_SCHOOL_LOCK "\xee\x95\xaf"	// U+e56f
+#define ICON_FA_SCISSORS "\xef\x83\x84"	// U+f0c4
+#define ICON_FA_SCREWDRIVER "\xef\x95\x8a"	// U+f54a
+#define ICON_FA_SCREWDRIVER_WRENCH "\xef\x9f\x99"	// U+f7d9
+#define ICON_FA_SCROLL "\xef\x9c\x8e"	// U+f70e
+#define ICON_FA_SCROLL_TORAH "\xef\x9a\xa0"	// U+f6a0
+#define ICON_FA_SD_CARD "\xef\x9f\x82"	// U+f7c2
+#define ICON_FA_SECTION "\xee\x91\x87"	// U+e447
+#define ICON_FA_SEEDLING "\xef\x93\x98"	// U+f4d8
+#define ICON_FA_SERVER "\xef\x88\xb3"	// U+f233
+#define ICON_FA_SHAPES "\xef\x98\x9f"	// U+f61f
+#define ICON_FA_SHARE "\xef\x81\xa4"	// U+f064
+#define ICON_FA_SHARE_FROM_SQUARE "\xef\x85\x8d"	// U+f14d
+#define ICON_FA_SHARE_NODES "\xef\x87\xa0"	// U+f1e0
+#define ICON_FA_SHEET_PLASTIC "\xee\x95\xb1"	// U+e571
+#define ICON_FA_SHEKEL_SIGN "\xef\x88\x8b"	// U+f20b
+#define ICON_FA_SHIELD "\xef\x84\xb2"	// U+f132
+#define ICON_FA_SHIELD_CAT "\xee\x95\xb2"	// U+e572
+#define ICON_FA_SHIELD_DOG "\xee\x95\xb3"	// U+e573
+#define ICON_FA_SHIELD_HALVED "\xef\x8f\xad"	// U+f3ed
+#define ICON_FA_SHIELD_HEART "\xee\x95\xb4"	// U+e574
+#define ICON_FA_SHIELD_VIRUS "\xee\x81\xac"	// U+e06c
+#define ICON_FA_SHIP "\xef\x88\x9a"	// U+f21a
+#define ICON_FA_SHIRT "\xef\x95\x93"	// U+f553
+#define ICON_FA_SHOE_PRINTS "\xef\x95\x8b"	// U+f54b
+#define ICON_FA_SHOP "\xef\x95\x8f"	// U+f54f
+#define ICON_FA_SHOP_LOCK "\xee\x92\xa5"	// U+e4a5
+#define ICON_FA_SHOP_SLASH "\xee\x81\xb0"	// U+e070
+#define ICON_FA_SHOWER "\xef\x8b\x8c"	// U+f2cc
+#define ICON_FA_SHRIMP "\xee\x91\x88"	// U+e448
+#define ICON_FA_SHUFFLE "\xef\x81\xb4"	// U+f074
+#define ICON_FA_SHUTTLE_SPACE "\xef\x86\x97"	// U+f197
+#define ICON_FA_SIGN_HANGING "\xef\x93\x99"	// U+f4d9
+#define ICON_FA_SIGNAL "\xef\x80\x92"	// U+f012
+#define ICON_FA_SIGNATURE "\xef\x96\xb7"	// U+f5b7
+#define ICON_FA_SIGNS_POST "\xef\x89\xb7"	// U+f277
+#define ICON_FA_SIM_CARD "\xef\x9f\x84"	// U+f7c4
+#define ICON_FA_SINK "\xee\x81\xad"	// U+e06d
+#define ICON_FA_SITEMAP "\xef\x83\xa8"	// U+f0e8
+#define ICON_FA_SKULL "\xef\x95\x8c"	// U+f54c
+#define ICON_FA_SKULL_CROSSBONES "\xef\x9c\x94"	// U+f714
+#define ICON_FA_SLASH "\xef\x9c\x95"	// U+f715
+#define ICON_FA_SLEIGH "\xef\x9f\x8c"	// U+f7cc
+#define ICON_FA_SLIDERS "\xef\x87\x9e"	// U+f1de
+#define ICON_FA_SMOG "\xef\x9d\x9f"	// U+f75f
+#define ICON_FA_SMOKING "\xef\x92\x8d"	// U+f48d
+#define ICON_FA_SNOWFLAKE "\xef\x8b\x9c"	// U+f2dc
+#define ICON_FA_SNOWMAN "\xef\x9f\x90"	// U+f7d0
+#define ICON_FA_SNOWPLOW "\xef\x9f\x92"	// U+f7d2
+#define ICON_FA_SOAP "\xee\x81\xae"	// U+e06e
+#define ICON_FA_SOCKS "\xef\x9a\x96"	// U+f696
+#define ICON_FA_SOLAR_PANEL "\xef\x96\xba"	// U+f5ba
+#define ICON_FA_SORT "\xef\x83\x9c"	// U+f0dc
+#define ICON_FA_SORT_DOWN "\xef\x83\x9d"	// U+f0dd
+#define ICON_FA_SORT_UP "\xef\x83\x9e"	// U+f0de
+#define ICON_FA_SPA "\xef\x96\xbb"	// U+f5bb
+#define ICON_FA_SPAGHETTI_MONSTER_FLYING "\xef\x99\xbb"	// U+f67b
+#define ICON_FA_SPELL_CHECK "\xef\xa2\x91"	// U+f891
+#define ICON_FA_SPIDER "\xef\x9c\x97"	// U+f717
+#define ICON_FA_SPINNER "\xef\x84\x90"	// U+f110
+#define ICON_FA_SPLOTCH "\xef\x96\xbc"	// U+f5bc
+#define ICON_FA_SPOON "\xef\x8b\xa5"	// U+f2e5
+#define ICON_FA_SPRAY_CAN "\xef\x96\xbd"	// U+f5bd
+#define ICON_FA_SPRAY_CAN_SPARKLES "\xef\x97\x90"	// U+f5d0
+#define ICON_FA_SQUARE "\xef\x83\x88"	// U+f0c8
+#define ICON_FA_SQUARE_ARROW_UP_RIGHT "\xef\x85\x8c"	// U+f14c
+#define ICON_FA_SQUARE_CARET_DOWN "\xef\x85\x90"	// U+f150
+#define ICON_FA_SQUARE_CARET_LEFT "\xef\x86\x91"	// U+f191
+#define ICON_FA_SQUARE_CARET_RIGHT "\xef\x85\x92"	// U+f152
+#define ICON_FA_SQUARE_CARET_UP "\xef\x85\x91"	// U+f151
+#define ICON_FA_SQUARE_CHECK "\xef\x85\x8a"	// U+f14a
+#define ICON_FA_SQUARE_ENVELOPE "\xef\x86\x99"	// U+f199
+#define ICON_FA_SQUARE_FULL "\xef\x91\x9c"	// U+f45c
+#define ICON_FA_SQUARE_H "\xef\x83\xbd"	// U+f0fd
+#define ICON_FA_SQUARE_MINUS "\xef\x85\x86"	// U+f146
+#define ICON_FA_SQUARE_NFI "\xee\x95\xb6"	// U+e576
+#define ICON_FA_SQUARE_PARKING "\xef\x95\x80"	// U+f540
+#define ICON_FA_SQUARE_PEN "\xef\x85\x8b"	// U+f14b
+#define ICON_FA_SQUARE_PERSON_CONFINED "\xee\x95\xb7"	// U+e577
+#define ICON_FA_SQUARE_PHONE "\xef\x82\x98"	// U+f098
+#define ICON_FA_SQUARE_PHONE_FLIP "\xef\xa1\xbb"	// U+f87b
+#define ICON_FA_SQUARE_PLUS "\xef\x83\xbe"	// U+f0fe
+#define ICON_FA_SQUARE_POLL_HORIZONTAL "\xef\x9a\x82"	// U+f682
+#define ICON_FA_SQUARE_POLL_VERTICAL "\xef\x9a\x81"	// U+f681
+#define ICON_FA_SQUARE_ROOT_VARIABLE "\xef\x9a\x98"	// U+f698
+#define ICON_FA_SQUARE_RSS "\xef\x85\x83"	// U+f143
+#define ICON_FA_SQUARE_SHARE_NODES "\xef\x87\xa1"	// U+f1e1
+#define ICON_FA_SQUARE_UP_RIGHT "\xef\x8d\xa0"	// U+f360
+#define ICON_FA_SQUARE_VIRUS "\xee\x95\xb8"	// U+e578
+#define ICON_FA_SQUARE_XMARK "\xef\x8b\x93"	// U+f2d3
+#define ICON_FA_STAFF_AESCULAPIUS "\xee\x95\xb9"	// U+e579
+#define ICON_FA_STAIRS "\xee\x8a\x89"	// U+e289
+#define ICON_FA_STAMP "\xef\x96\xbf"	// U+f5bf
+#define ICON_FA_STAR "\xef\x80\x85"	// U+f005
+#define ICON_FA_STAR_AND_CRESCENT "\xef\x9a\x99"	// U+f699
+#define ICON_FA_STAR_HALF "\xef\x82\x89"	// U+f089
+#define ICON_FA_STAR_HALF_STROKE "\xef\x97\x80"	// U+f5c0
+#define ICON_FA_STAR_OF_DAVID "\xef\x9a\x9a"	// U+f69a
+#define ICON_FA_STAR_OF_LIFE "\xef\x98\xa1"	// U+f621
+#define ICON_FA_STERLING_SIGN "\xef\x85\x94"	// U+f154
+#define ICON_FA_STETHOSCOPE "\xef\x83\xb1"	// U+f0f1
+#define ICON_FA_STOP "\xef\x81\x8d"	// U+f04d
+#define ICON_FA_STOPWATCH "\xef\x8b\xb2"	// U+f2f2
+#define ICON_FA_STOPWATCH_20 "\xee\x81\xaf"	// U+e06f
+#define ICON_FA_STORE "\xef\x95\x8e"	// U+f54e
+#define ICON_FA_STORE_SLASH "\xee\x81\xb1"	// U+e071
+#define ICON_FA_STREET_VIEW "\xef\x88\x9d"	// U+f21d
+#define ICON_FA_STRIKETHROUGH "\xef\x83\x8c"	// U+f0cc
+#define ICON_FA_STROOPWAFEL "\xef\x95\x91"	// U+f551
+#define ICON_FA_SUBSCRIPT "\xef\x84\xac"	// U+f12c
+#define ICON_FA_SUITCASE "\xef\x83\xb2"	// U+f0f2
+#define ICON_FA_SUITCASE_MEDICAL "\xef\x83\xba"	// U+f0fa
+#define ICON_FA_SUITCASE_ROLLING "\xef\x97\x81"	// U+f5c1
+#define ICON_FA_SUN "\xef\x86\x85"	// U+f185
+#define ICON_FA_SUN_PLANT_WILT "\xee\x95\xba"	// U+e57a
+#define ICON_FA_SUPERSCRIPT "\xef\x84\xab"	// U+f12b
+#define ICON_FA_SWATCHBOOK "\xef\x97\x83"	// U+f5c3
+#define ICON_FA_SYNAGOGUE "\xef\x9a\x9b"	// U+f69b
+#define ICON_FA_SYRINGE "\xef\x92\x8e"	// U+f48e
+#define ICON_FA_T "T"	// U+54
+#define ICON_FA_TABLE "\xef\x83\x8e"	// U+f0ce
+#define ICON_FA_TABLE_CELLS "\xef\x80\x8a"	// U+f00a
+#define ICON_FA_TABLE_CELLS_LARGE "\xef\x80\x89"	// U+f009
+#define ICON_FA_TABLE_COLUMNS "\xef\x83\x9b"	// U+f0db
+#define ICON_FA_TABLE_LIST "\xef\x80\x8b"	// U+f00b
+#define ICON_FA_TABLE_TENNIS_PADDLE_BALL "\xef\x91\x9d"	// U+f45d
+#define ICON_FA_TABLET "\xef\x8f\xbb"	// U+f3fb
+#define ICON_FA_TABLET_BUTTON "\xef\x84\x8a"	// U+f10a
+#define ICON_FA_TABLET_SCREEN_BUTTON "\xef\x8f\xba"	// U+f3fa
+#define ICON_FA_TABLETS "\xef\x92\x90"	// U+f490
+#define ICON_FA_TACHOGRAPH_DIGITAL "\xef\x95\xa6"	// U+f566
+#define ICON_FA_TAG "\xef\x80\xab"	// U+f02b
+#define ICON_FA_TAGS "\xef\x80\xac"	// U+f02c
+#define ICON_FA_TAPE "\xef\x93\x9b"	// U+f4db
+#define ICON_FA_TARP "\xee\x95\xbb"	// U+e57b
+#define ICON_FA_TARP_DROPLET "\xee\x95\xbc"	// U+e57c
+#define ICON_FA_TAXI "\xef\x86\xba"	// U+f1ba
+#define ICON_FA_TEETH "\xef\x98\xae"	// U+f62e
+#define ICON_FA_TEETH_OPEN "\xef\x98\xaf"	// U+f62f
+#define ICON_FA_TEMPERATURE_ARROW_DOWN "\xee\x80\xbf"	// U+e03f
+#define ICON_FA_TEMPERATURE_ARROW_UP "\xee\x81\x80"	// U+e040
+#define ICON_FA_TEMPERATURE_EMPTY "\xef\x8b\x8b"	// U+f2cb
+#define ICON_FA_TEMPERATURE_FULL "\xef\x8b\x87"	// U+f2c7
+#define ICON_FA_TEMPERATURE_HALF "\xef\x8b\x89"	// U+f2c9
+#define ICON_FA_TEMPERATURE_HIGH "\xef\x9d\xa9"	// U+f769
+#define ICON_FA_TEMPERATURE_LOW "\xef\x9d\xab"	// U+f76b
+#define ICON_FA_TEMPERATURE_QUARTER "\xef\x8b\x8a"	// U+f2ca
+#define ICON_FA_TEMPERATURE_THREE_QUARTERS "\xef\x8b\x88"	// U+f2c8
+#define ICON_FA_TENGE_SIGN "\xef\x9f\x97"	// U+f7d7
+#define ICON_FA_TENT "\xee\x95\xbd"	// U+e57d
+#define ICON_FA_TENT_ARROW_DOWN_TO_LINE "\xee\x95\xbe"	// U+e57e
+#define ICON_FA_TENT_ARROW_LEFT_RIGHT "\xee\x95\xbf"	// U+e57f
+#define ICON_FA_TENT_ARROW_TURN_LEFT "\xee\x96\x80"	// U+e580
+#define ICON_FA_TENT_ARROWS_DOWN "\xee\x96\x81"	// U+e581
+#define ICON_FA_TENTS "\xee\x96\x82"	// U+e582
+#define ICON_FA_TERMINAL "\xef\x84\xa0"	// U+f120
+#define ICON_FA_TEXT_HEIGHT "\xef\x80\xb4"	// U+f034
+#define ICON_FA_TEXT_SLASH "\xef\xa1\xbd"	// U+f87d
+#define ICON_FA_TEXT_WIDTH "\xef\x80\xb5"	// U+f035
+#define ICON_FA_THERMOMETER "\xef\x92\x91"	// U+f491
+#define ICON_FA_THUMBS_DOWN "\xef\x85\xa5"	// U+f165
+#define ICON_FA_THUMBS_UP "\xef\x85\xa4"	// U+f164
+#define ICON_FA_THUMBTACK "\xef\x82\x8d"	// U+f08d
+#define ICON_FA_TICKET "\xef\x85\x85"	// U+f145
+#define ICON_FA_TICKET_SIMPLE "\xef\x8f\xbf"	// U+f3ff
+#define ICON_FA_TIMELINE "\xee\x8a\x9c"	// U+e29c
+#define ICON_FA_TOGGLE_OFF "\xef\x88\x84"	// U+f204
+#define ICON_FA_TOGGLE_ON "\xef\x88\x85"	// U+f205
+#define ICON_FA_TOILET "\xef\x9f\x98"	// U+f7d8
+#define ICON_FA_TOILET_PAPER "\xef\x9c\x9e"	// U+f71e
+#define ICON_FA_TOILET_PAPER_SLASH "\xee\x81\xb2"	// U+e072
+#define ICON_FA_TOILET_PORTABLE "\xee\x96\x83"	// U+e583
+#define ICON_FA_TOILETS_PORTABLE "\xee\x96\x84"	// U+e584
+#define ICON_FA_TOOLBOX "\xef\x95\x92"	// U+f552
+#define ICON_FA_TOOTH "\xef\x97\x89"	// U+f5c9
+#define ICON_FA_TORII_GATE "\xef\x9a\xa1"	// U+f6a1
+#define ICON_FA_TORNADO "\xef\x9d\xaf"	// U+f76f
+#define ICON_FA_TOWER_BROADCAST "\xef\x94\x99"	// U+f519
+#define ICON_FA_TOWER_CELL "\xee\x96\x85"	// U+e585
+#define ICON_FA_TOWER_OBSERVATION "\xee\x96\x86"	// U+e586
+#define ICON_FA_TRACTOR "\xef\x9c\xa2"	// U+f722
+#define ICON_FA_TRADEMARK "\xef\x89\x9c"	// U+f25c
+#define ICON_FA_TRAFFIC_LIGHT "\xef\x98\xb7"	// U+f637
+#define ICON_FA_TRAILER "\xee\x81\x81"	// U+e041
+#define ICON_FA_TRAIN "\xef\x88\xb8"	// U+f238
+#define ICON_FA_TRAIN_SUBWAY "\xef\x88\xb9"	// U+f239
+#define ICON_FA_TRAIN_TRAM "\xef\x9f\x9a"	// U+f7da
+#define ICON_FA_TRANSGENDER "\xef\x88\xa5"	// U+f225
+#define ICON_FA_TRASH "\xef\x87\xb8"	// U+f1f8
+#define ICON_FA_TRASH_ARROW_UP "\xef\xa0\xa9"	// U+f829
+#define ICON_FA_TRASH_CAN "\xef\x8b\xad"	// U+f2ed
+#define ICON_FA_TRASH_CAN_ARROW_UP "\xef\xa0\xaa"	// U+f82a
+#define ICON_FA_TREE "\xef\x86\xbb"	// U+f1bb
+#define ICON_FA_TREE_CITY "\xee\x96\x87"	// U+e587
+#define ICON_FA_TRIANGLE_EXCLAMATION "\xef\x81\xb1"	// U+f071
+#define ICON_FA_TROPHY "\xef\x82\x91"	// U+f091
+#define ICON_FA_TROWEL "\xee\x96\x89"	// U+e589
+#define ICON_FA_TROWEL_BRICKS "\xee\x96\x8a"	// U+e58a
+#define ICON_FA_TRUCK "\xef\x83\x91"	// U+f0d1
+#define ICON_FA_TRUCK_ARROW_RIGHT "\xee\x96\x8b"	// U+e58b
+#define ICON_FA_TRUCK_DROPLET "\xee\x96\x8c"	// U+e58c
+#define ICON_FA_TRUCK_FAST "\xef\x92\x8b"	// U+f48b
+#define ICON_FA_TRUCK_FIELD "\xee\x96\x8d"	// U+e58d
+#define ICON_FA_TRUCK_FIELD_UN "\xee\x96\x8e"	// U+e58e
+#define ICON_FA_TRUCK_FRONT "\xee\x8a\xb7"	// U+e2b7
+#define ICON_FA_TRUCK_MEDICAL "\xef\x83\xb9"	// U+f0f9
+#define ICON_FA_TRUCK_MONSTER "\xef\x98\xbb"	// U+f63b
+#define ICON_FA_TRUCK_MOVING "\xef\x93\x9f"	// U+f4df
+#define ICON_FA_TRUCK_PICKUP "\xef\x98\xbc"	// U+f63c
+#define ICON_FA_TRUCK_PLANE "\xee\x96\x8f"	// U+e58f
+#define ICON_FA_TRUCK_RAMP_BOX "\xef\x93\x9e"	// U+f4de
+#define ICON_FA_TTY "\xef\x87\xa4"	// U+f1e4
+#define ICON_FA_TURKISH_LIRA_SIGN "\xee\x8a\xbb"	// U+e2bb
+#define ICON_FA_TURN_DOWN "\xef\x8e\xbe"	// U+f3be
+#define ICON_FA_TURN_UP "\xef\x8e\xbf"	// U+f3bf
+#define ICON_FA_TV "\xef\x89\xac"	// U+f26c
+#define ICON_FA_U "U"	// U+55
+#define ICON_FA_UMBRELLA "\xef\x83\xa9"	// U+f0e9
+#define ICON_FA_UMBRELLA_BEACH "\xef\x97\x8a"	// U+f5ca
+#define ICON_FA_UNDERLINE "\xef\x83\x8d"	// U+f0cd
+#define ICON_FA_UNIVERSAL_ACCESS "\xef\x8a\x9a"	// U+f29a
+#define ICON_FA_UNLOCK "\xef\x82\x9c"	// U+f09c
+#define ICON_FA_UNLOCK_KEYHOLE "\xef\x84\xbe"	// U+f13e
+#define ICON_FA_UP_DOWN "\xef\x8c\xb8"	// U+f338
+#define ICON_FA_UP_DOWN_LEFT_RIGHT "\xef\x82\xb2"	// U+f0b2
+#define ICON_FA_UP_LONG "\xef\x8c\x8c"	// U+f30c
+#define ICON_FA_UP_RIGHT_AND_DOWN_LEFT_FROM_CENTER "\xef\x90\xa4"	// U+f424
+#define ICON_FA_UP_RIGHT_FROM_SQUARE "\xef\x8d\x9d"	// U+f35d
+#define ICON_FA_UPLOAD "\xef\x82\x93"	// U+f093
+#define ICON_FA_USER "\xef\x80\x87"	// U+f007
+#define ICON_FA_USER_ASTRONAUT "\xef\x93\xbb"	// U+f4fb
+#define ICON_FA_USER_CHECK "\xef\x93\xbc"	// U+f4fc
+#define ICON_FA_USER_CLOCK "\xef\x93\xbd"	// U+f4fd
+#define ICON_FA_USER_DOCTOR "\xef\x83\xb0"	// U+f0f0
+#define ICON_FA_USER_GEAR "\xef\x93\xbe"	// U+f4fe
+#define ICON_FA_USER_GRADUATE "\xef\x94\x81"	// U+f501
+#define ICON_FA_USER_GROUP "\xef\x94\x80"	// U+f500
+#define ICON_FA_USER_INJURED "\xef\x9c\xa8"	// U+f728
+#define ICON_FA_USER_LARGE "\xef\x90\x86"	// U+f406
+#define ICON_FA_USER_LARGE_SLASH "\xef\x93\xba"	// U+f4fa
+#define ICON_FA_USER_LOCK "\xef\x94\x82"	// U+f502
+#define ICON_FA_USER_MINUS "\xef\x94\x83"	// U+f503
+#define ICON_FA_USER_NINJA "\xef\x94\x84"	// U+f504
+#define ICON_FA_USER_NURSE "\xef\xa0\xaf"	// U+f82f
+#define ICON_FA_USER_PEN "\xef\x93\xbf"	// U+f4ff
+#define ICON_FA_USER_PLUS "\xef\x88\xb4"	// U+f234
+#define ICON_FA_USER_SECRET "\xef\x88\x9b"	// U+f21b
+#define ICON_FA_USER_SHIELD "\xef\x94\x85"	// U+f505
+#define ICON_FA_USER_SLASH "\xef\x94\x86"	// U+f506
+#define ICON_FA_USER_TAG "\xef\x94\x87"	// U+f507
+#define ICON_FA_USER_TIE "\xef\x94\x88"	// U+f508
+#define ICON_FA_USER_XMARK "\xef\x88\xb5"	// U+f235
+#define ICON_FA_USERS "\xef\x83\x80"	// U+f0c0
+#define ICON_FA_USERS_BETWEEN_LINES "\xee\x96\x91"	// U+e591
+#define ICON_FA_USERS_GEAR "\xef\x94\x89"	// U+f509
+#define ICON_FA_USERS_LINE "\xee\x96\x92"	// U+e592
+#define ICON_FA_USERS_RAYS "\xee\x96\x93"	// U+e593
+#define ICON_FA_USERS_RECTANGLE "\xee\x96\x94"	// U+e594
+#define ICON_FA_USERS_SLASH "\xee\x81\xb3"	// U+e073
+#define ICON_FA_USERS_VIEWFINDER "\xee\x96\x95"	// U+e595
+#define ICON_FA_UTENSILS "\xef\x8b\xa7"	// U+f2e7
+#define ICON_FA_V "V"	// U+56
+#define ICON_FA_VAN_SHUTTLE "\xef\x96\xb6"	// U+f5b6
+#define ICON_FA_VAULT "\xee\x8b\x85"	// U+e2c5
+#define ICON_FA_VECTOR_SQUARE "\xef\x97\x8b"	// U+f5cb
+#define ICON_FA_VENUS "\xef\x88\xa1"	// U+f221
+#define ICON_FA_VENUS_DOUBLE "\xef\x88\xa6"	// U+f226
+#define ICON_FA_VENUS_MARS "\xef\x88\xa8"	// U+f228
+#define ICON_FA_VEST "\xee\x82\x85"	// U+e085
+#define ICON_FA_VEST_PATCHES "\xee\x82\x86"	// U+e086
+#define ICON_FA_VIAL "\xef\x92\x92"	// U+f492
+#define ICON_FA_VIAL_CIRCLE_CHECK "\xee\x96\x96"	// U+e596
+#define ICON_FA_VIAL_VIRUS "\xee\x96\x97"	// U+e597
+#define ICON_FA_VIALS "\xef\x92\x93"	// U+f493
+#define ICON_FA_VIDEO "\xef\x80\xbd"	// U+f03d
+#define ICON_FA_VIDEO_SLASH "\xef\x93\xa2"	// U+f4e2
+#define ICON_FA_VIHARA "\xef\x9a\xa7"	// U+f6a7
+#define ICON_FA_VIRUS "\xee\x81\xb4"	// U+e074
+#define ICON_FA_VIRUS_COVID "\xee\x92\xa8"	// U+e4a8
+#define ICON_FA_VIRUS_COVID_SLASH "\xee\x92\xa9"	// U+e4a9
+#define ICON_FA_VIRUS_SLASH "\xee\x81\xb5"	// U+e075
+#define ICON_FA_VIRUSES "\xee\x81\xb6"	// U+e076
+#define ICON_FA_VOICEMAIL "\xef\xa2\x97"	// U+f897
+#define ICON_FA_VOLCANO "\xef\x9d\xb0"	// U+f770
+#define ICON_FA_VOLLEYBALL "\xef\x91\x9f"	// U+f45f
+#define ICON_FA_VOLUME_HIGH "\xef\x80\xa8"	// U+f028
+#define ICON_FA_VOLUME_LOW "\xef\x80\xa7"	// U+f027
+#define ICON_FA_VOLUME_OFF "\xef\x80\xa6"	// U+f026
+#define ICON_FA_VOLUME_XMARK "\xef\x9a\xa9"	// U+f6a9
+#define ICON_FA_VR_CARDBOARD "\xef\x9c\xa9"	// U+f729
+#define ICON_FA_W "W"	// U+57
+#define ICON_FA_WALKIE_TALKIE "\xef\xa3\xaf"	// U+f8ef
+#define ICON_FA_WALLET "\xef\x95\x95"	// U+f555
+#define ICON_FA_WAND_MAGIC "\xef\x83\x90"	// U+f0d0
+#define ICON_FA_WAND_MAGIC_SPARKLES "\xee\x8b\x8a"	// U+e2ca
+#define ICON_FA_WAND_SPARKLES "\xef\x9c\xab"	// U+f72b
+#define ICON_FA_WAREHOUSE "\xef\x92\x94"	// U+f494
+#define ICON_FA_WATER "\xef\x9d\xb3"	// U+f773
+#define ICON_FA_WATER_LADDER "\xef\x97\x85"	// U+f5c5
+#define ICON_FA_WAVE_SQUARE "\xef\xa0\xbe"	// U+f83e
+#define ICON_FA_WEIGHT_HANGING "\xef\x97\x8d"	// U+f5cd
+#define ICON_FA_WEIGHT_SCALE "\xef\x92\x96"	// U+f496
+#define ICON_FA_WHEAT_AWN "\xee\x8b\x8d"	// U+e2cd
+#define ICON_FA_WHEAT_AWN_CIRCLE_EXCLAMATION "\xee\x96\x98"	// U+e598
+#define ICON_FA_WHEELCHAIR "\xef\x86\x93"	// U+f193
+#define ICON_FA_WHEELCHAIR_MOVE "\xee\x8b\x8e"	// U+e2ce
+#define ICON_FA_WHISKEY_GLASS "\xef\x9e\xa0"	// U+f7a0
+#define ICON_FA_WIFI "\xef\x87\xab"	// U+f1eb
+#define ICON_FA_WIND "\xef\x9c\xae"	// U+f72e
+#define ICON_FA_WINDOW_MAXIMIZE "\xef\x8b\x90"	// U+f2d0
+#define ICON_FA_WINDOW_MINIMIZE "\xef\x8b\x91"	// U+f2d1
+#define ICON_FA_WINDOW_RESTORE "\xef\x8b\x92"	// U+f2d2
+#define ICON_FA_WINE_BOTTLE "\xef\x9c\xaf"	// U+f72f
+#define ICON_FA_WINE_GLASS "\xef\x93\xa3"	// U+f4e3
+#define ICON_FA_WINE_GLASS_EMPTY "\xef\x97\x8e"	// U+f5ce
+#define ICON_FA_WON_SIGN "\xef\x85\x99"	// U+f159
+#define ICON_FA_WORM "\xee\x96\x99"	// U+e599
+#define ICON_FA_WRENCH "\xef\x82\xad"	// U+f0ad
+#define ICON_FA_X "X"	// U+58
+#define ICON_FA_X_RAY "\xef\x92\x97"	// U+f497
+#define ICON_FA_XMARK "\xef\x80\x8d"	// U+f00d
+#define ICON_FA_XMARKS_LINES "\xee\x96\x9a"	// U+e59a
+#define ICON_FA_Y "Y"	// U+59
+#define ICON_FA_YEN_SIGN "\xef\x85\x97"	// U+f157
+#define ICON_FA_YIN_YANG "\xef\x9a\xad"	// U+f6ad
+#define ICON_FA_Z "Z"	// U+5a
diff --git a/ui/thirdparty/fpng/HEAD b/ui/thirdparty/fpng/HEAD
new file mode 100644
index 0000000000..5adb30a452
--- /dev/null
+++ b/ui/thirdparty/fpng/HEAD
@@ -0,0 +1 @@
+645d49cf6b2e82ce25b5b59f6a2e2df30e6f5fa6
diff --git a/ui/thirdparty/fpng/fpng.cpp b/ui/thirdparty/fpng/fpng.cpp
new file mode 100644
index 0000000000..0b34e1cfcb
--- /dev/null
+++ b/ui/thirdparty/fpng/fpng.cpp
@@ -0,0 +1,3222 @@
+// fpng.cpp 1.0.6 - Fast 24/32bpp .PNG image writer/reader. See unlicense at the end of this file.
+// PNG's generated by this code have been tested to load successfully with stb_image.h, lodepng.cpp, wuffs, libpng, and pngcheck.
+//
+// Uses code from the simple PNG writer function by Alex Evans, 2011. Released into the public domain: https://gist.github.com/908299
+// Some low-level Deflate/Huffman functions derived from the original 2011 Google Code version of miniz (public domain by R. Geldreich, Jr.): https://code.google.com/archive/p/miniz/
+// Low-level Huffman code size function: public domain, originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996.
+//
+// Optional config macros:
+// FPNG_NO_SSE - Set to 1 to completely disable SSE usage, even on x86/x64. By default, on x86/x64 it's enabled.
+// FPNG_DISABLE_DECODE_CRC32_CHECKS - Set to 1 to disable PNG chunk CRC-32 tests, for improved fuzzing. Defaults to 0.
+// FPNG_USE_UNALIGNED_LOADS - Set to 1 to indicate it's OK to read/write unaligned 32-bit/64-bit values. Defaults to 0, unless x86/x64.
+//
+// With gcc/clang on x86, compile with -msse4.1 -mpclmul -fno-strict-aliasing
+// Only tested with -fno-strict-aliasing (which the Linux kernel uses, and MSVC's default).
+//
+#include "fpng.h"
+#include <assert.h>
+#include <string.h>
+
+#ifdef _MSC_VER
+	#pragma warning (disable:4127) // conditional expression is constant
+#endif
+
+// Set FPNG_NO_SSE to 1 to completely disable SSE usage.
+#ifndef FPNG_NO_SSE
+	#define FPNG_NO_SSE (0)
+#endif
+
+// Detect if we're compiling on x86/x64
+#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__)
+	#define FPNG_X86_OR_X64_CPU (1)
+#else
+	#define FPNG_X86_OR_X64_CPU (0)
+#endif
+
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE
+	#ifdef _MSC_VER
+		#include <intrin.h>
+	#endif
+	#include <xmmintrin.h>		// SSE
+	#include <emmintrin.h>		// SSE2
+	#include <smmintrin.h>		// SSE4.1
+	#include <wmmintrin.h>		// pclmul
+#endif
+
+#ifndef FPNG_NO_STDIO
+	#include <stdio.h>
+#endif
+
+// Allow the disabling of the chunk data CRC32 checks, for fuzz testing of the decoder
+#ifndef FPNG_DISABLE_DECODE_CRC32_CHECKS
+	#define FPNG_DISABLE_DECODE_CRC32_CHECKS (0)
+#endif
+
+// Using unaligned loads and stores causes errors when using UBSan. Jam it off.
+#if defined(__has_feature)
+	#if __has_feature(undefined_behavior_sanitizer)
+		#undef FPNG_USE_UNALIGNED_LOADS
+		#define FPNG_USE_UNALIGNED_LOADS (0)
+	#endif
+#endif
+
+// Set to 0 if your platform doesn't support unaligned 32-bit/64-bit reads/writes. 
+#ifndef FPNG_USE_UNALIGNED_LOADS
+	#if FPNG_X86_OR_X64_CPU
+		// On x86/x64 we default to enabled, for a noticeable perf gain.
+		#define FPNG_USE_UNALIGNED_LOADS (1)
+	#else
+		#define FPNG_USE_UNALIGNED_LOADS (0)
+	#endif
+#endif
+
+#if defined(_MSC_VER) || defined(__MINGW32__) || FPNG_X86_OR_X64_CPU
+	#ifndef __LITTLE_ENDIAN
+	#define __LITTLE_ENDIAN 1234
+	#endif
+	#ifndef __BIG_ENDIAN
+	#define __BIG_ENDIAN 4321
+	#endif
+
+	// Assume little endian on Windows/x86/x64.
+	#define __BYTE_ORDER __LITTLE_ENDIAN
+#elif defined(__APPLE__)
+	#define __BYTE_ORDER __BYTE_ORDER__
+	#define __LITTLE_ENDIAN __LITTLE_ENDIAN__
+	#define __BIG_ENDIAN __BIG_ENDIAN__
+#else
+	// for __BYTE_ORDER (__LITTLE_ENDIAN or __BIG_ENDIAN)
+	#include <sys/param.h>
+
+	#ifndef __LITTLE_ENDIAN
+	#define __LITTLE_ENDIAN 1234
+	#endif
+	#ifndef __BIG_ENDIAN
+	#define __BIG_ENDIAN 4321
+	#endif
+#endif
+
+#if !defined(__BYTE_ORDER)
+	#error __BYTE_ORDER undefined. Compile with -D__BYTE_ORDER=1234 for little endian or -D__BYTE_ORDER=4321 for big endian.
+#endif
+
+namespace fpng
+{
+	static const int FPNG_FALSE = 0;
+	static const uint8_t FPNG_FDEC_VERSION = 0;
+	static const uint32_t FPNG_MAX_SUPPORTED_DIM = 1 << 24;
+
+	template <typename S> static inline S maximum(S a, S b) { return (a > b) ? a : b; }
+	template <typename S> static inline S minimum(S a, S b) { return (a < b) ? a : b; }
+
+	static inline uint32_t simple_swap32(uint32_t x) { return (x >> 24) | ((x >> 8) & 0x0000FF00) | ((x << 8) & 0x00FF0000) | (x << 24); }
+	static inline uint64_t simple_swap64(uint64_t x) { return (((uint64_t)simple_swap32((uint32_t)x)) << 32U) | simple_swap32((uint32_t)(x >> 32U)); }
+
+	static inline uint32_t swap32(uint32_t x)
+	{
+#if defined(__GNUC__) || defined(__clang__)
+		return __builtin_bswap32(x);
+#else
+		return simple_swap32(x);
+#endif
+	}
+
+	static inline uint64_t swap64(uint64_t x)
+	{
+#if defined(__GNUC__) || defined(__clang__)
+		return __builtin_bswap64(x);
+#else
+		return simple_swap64(x);
+#endif
+	}
+
+#if FPNG_USE_UNALIGNED_LOADS
+	#if __BYTE_ORDER == __BIG_ENDIAN
+		#define READ_LE32(p) swap32(*reinterpret_cast<const uint32_t *>(p))
+		#define WRITE_LE32(p, v) *reinterpret_cast<uint32_t *>(p) = swap32((uint32_t)(v))
+		#define WRITE_LE64(p, v) *reinterpret_cast<uint64_t *>(p) = swap64((uint64_t)(v))
+
+		#define READ_BE32(p) *reinterpret_cast<const uint32_t *>(p)
+	#else
+		#define READ_LE32(p) (*reinterpret_cast<const uint32_t *>(p))
+		#define WRITE_LE32(p, v) *reinterpret_cast<uint32_t *>(p) = (uint32_t)(v)
+		#define WRITE_LE64(p, v) *reinterpret_cast<uint64_t *>(p) = (uint64_t)(v)
+
+		#define READ_BE32(p) swap32(*reinterpret_cast<const uint32_t *>(p))
+	#endif
+#else
+	// A good compiler should be able to optimize these routines - hopefully. They are crucial for performance.
+	static inline uint32_t READ_LE32(const void* p)
+	{
+		const uint8_t* pBytes = (const uint8_t*)p;
+		return ((uint32_t)pBytes[0]) | (((uint32_t)pBytes[1]) << 8U) | (((uint32_t)pBytes[2]) << 16U) | (((uint32_t)pBytes[3]) << 24U);
+	}
+
+	static inline uint32_t READ_BE32(const void* p)
+	{
+		const uint8_t* pBytes = (const uint8_t*)p;
+		return ((uint32_t)pBytes[3]) | (((uint32_t)pBytes[2]) << 8U) | (((uint32_t)pBytes[1]) << 16U) | (((uint32_t)pBytes[0]) << 24U);
+	}
+
+	static inline void WRITE_LE32(const void* p, uint32_t v)
+	{
+		uint8_t* pBytes = (uint8_t*)p;
+		pBytes[0] = (uint8_t)(v);
+		pBytes[1] = (uint8_t)(v >> 8);
+		pBytes[2] = (uint8_t)(v >> 16);
+		pBytes[3] = (uint8_t)(v >> 24);
+	}
+
+	static inline void WRITE_LE64(const void* p, uint64_t v)
+	{
+		uint8_t* pBytes = (uint8_t*)p;
+		pBytes[0] = (uint8_t)(v);
+		pBytes[1] = (uint8_t)(v >> 8);
+		pBytes[2] = (uint8_t)(v >> 16);
+		pBytes[3] = (uint8_t)(v >> 24);
+		pBytes[4] = (uint8_t)(v >> 32);
+		pBytes[5] = (uint8_t)(v >> 40);
+		pBytes[6] = (uint8_t)(v >> 48);
+		pBytes[7] = (uint8_t)(v >> 56);
+	}
+#endif
+
+	// Customized the very common case of reading a 24bpp pixel from memory
+	static inline uint32_t READ_RGB_PIXEL(const void* p)
+	{
+#if FPNG_USE_UNALIGNED_LOADS 
+		return READ_LE32(p) & 0xFFFFFF;
+#else
+		const uint8_t* pBytes = (const uint8_t*)p;
+		return ((uint32_t)pBytes[0]) | (((uint32_t)pBytes[1]) << 8U) | (((uint32_t)pBytes[2]) << 16U);
+#endif
+	}
+
+	// See "Slicing by 4" CRC-32 algorithm here: 
+	// https://create.stephan-brumme.com/crc32/
+
+	// Precomputed 4KB of CRC-32 tables
+	static const uint32_t g_crc32_4[4][256] = {
+	{00, 016701630226, 035603460454, 023102250672, 0733342031, 016032572217, 035130722465, 023631112643, 01666704062, 017167134244, 034065364436, 022764554610, 01155446053, 017654276275, 034756026407, 022057616621, 03555610144, 015254020362, 036356270510, 020457440736, 03266552175, 015567362353, 036465132521, 020364702707, 02333114126, 014432724300, 037530574572, 021231344754, 02400256117, 014301466331, 037203636543, 021502006765,
+	07333420310, 011432210136, 032530040744, 024231670562, 07400762321, 011301152107, 032203302775, 024502532553, 06555324372, 010254514154, 033356744726, 025457174500, 06266066343, 010567656165, 033465406717, 025364236531, 04666230254, 012167400072, 031065650600, 027764060426, 04155172265, 012654742043, 031756512631, 027057322417, 05000534236, 013701304010, 030603154662, 026102764444, 05733676207, 013032046021, 030130216653, 026631426475,
+	016667040620, 0166670406, 023064420274, 035765210052, 016154302611, 0655532437, 023757762245, 035056152063, 017001744642, 01700174464, 022602324216, 034103514030, 017732406673, 01033236455, 022131066227, 034630656001, 015332650764, 03433060542, 020531230330, 036230400116, 015401512755, 03300322573, 020202172301, 036503742127, 014554154706, 02255764520, 021357534352, 037456304174, 014267216737, 02566426511, 021464676363, 037365046145,
+	011554460530, 07255250716, 024357000164, 032456630342, 011267722501, 07566112727, 024464342155, 032365572373, 010332364552, 06433554774, 025531704106, 033230134320, 010401026563, 06300616745, 025202446137, 033503276311, 012001270474, 04700440652, 027602610020, 031103020206, 012732132445, 04033702663, 027131552011, 031630362237, 013667574416, 05166344630, 026064114042, 030765724264, 013154636427, 05655006601, 026757256073, 030056466255,
+	035556101440, 023257731666, 0355561014, 016454351232, 035265243471, 023564473657, 0466623025, 016367013203, 034330605422, 022431035604, 01533265076, 017232455250, 034403547413, 022302377635, 01200127047, 017501717261, 036003711504, 020702121722, 03600371150, 015101541376, 036730453535, 020031263713, 03133033161, 015632603347, 037665015566, 021164625740, 02066475132, 014767245314, 037156357557, 021657567771, 02755737103, 014054107325,
+	032665521750, 024164311576, 07066141304, 011767771122, 032156663761, 024657053547, 07755203335, 011054433113, 033003225732, 025702415514, 06600645366, 010101075140, 033730167703, 025031757525, 06133507357, 010632337171, 031330331614, 027431501432, 04533751240, 012232161066, 031403073625, 027302643403, 04200413271, 012501223057, 030556435676, 026257205450, 05355055222, 013454665004, 030265777647, 026564147461, 05466317213, 013367527035,
+	023331141260, 035430771046, 016532521634, 0233311412, 023402203251, 035303433077, 016201663605, 0500053423, 022557645202, 034256075024, 017354225656, 01455415470, 022264507233, 034565337015, 017467167667, 01366757441, 020664751324, 036165161102, 015067331770, 03766501556, 020157413315, 036656223133, 015754073741, 03055643567, 021002055346, 037703665160, 014601435712, 02100205534, 021731317377, 037030527151, 014132777723, 02633147505,
+	024002561170, 032703351356, 011601101524, 07100731702, 024731623141, 032030013367, 011132243515, 07633473733, 025664265112, 033165455334, 010067605546, 06766035760, 025157127123, 033656717305, 010754547577, 06055377751, 027557371034, 031256541212, 012354711460, 04455121646, 027264033005, 031565603223, 012467453451, 04366263677, 026331475056, 030430245270, 013532015402, 05233625624, 026402737067, 030303107241, 013201357433, 05500567615,
+	}, { 00,03106630501,06215461202,05313251703,014433142404,017535772105,012626523606,011720313307,031066305010,032160535511,037273764212,034375154713,025455247414,026553477115,023640626616,020746016317,011260411121,012366221420,017075070323,014173640622,05653553525,06755363024,03446132727,0540702226,020206714131,023300124430,026013375333,025115545632,034635656535,037733066034,032420237737,031526407236,
+	022541022242,021447612743,024754443040,027652273541,036172160646,035074750347,030367501444,033261331145,013527327252,010421517753,015732746050,016634176551,07114265656,04012455357,01301604454,02207034155,033721433363,030627203662,035534052161,036432662460,027312571767,024214341266,021107110565,022001720064,02747736373,01641106672,04552357171,07454567470,016374674777,015272044276,010161215575,013067425074,
+	036036247405,035130477104,030223626607,033325016306,022405305001,021503535500,024610764203,027716154702,07050142415,04156772114,01245523617,02343313316,013463000011,010565630510,015676461213,016770251712,027256656524,024350066025,021043237726,022145407227,033665714120,030763124421,035470375322,036576545623,016230553534,015336363035,010025132736,013123702237,02603411130,01705221431,04416070332,07510640633,
+	014577265647,017471455346,012762604445,011664034144,0144327243,03042517742,06351746041,05257176540,025511160657,026417750356,023704501455,020602331154,031122022253,032024612752,037337443051,034231273550,05717674766,06611044267,03502215564,0404425065,011324736362,012222106663,017131357160,014037567461,034771571776,037677341277,032564110574,031462720075,020342433372,023244203673,026157052170,025051662471,
+	07340714113,04246124412,01155375311,02053545610,013773656517,010675066016,015566237715,016460407214,036326411103,035220221402,030133070301,033035640600,022715553507,021613363006,024500132705,027406702204,016120305032,015026535533,010335764230,013233154731,02513247436,01415477137,04706626634,07600016335,027146000022,024040630523,021353461220,022255251721,033575142426,030473772127,035760523624,036666313325,
+	025601736351,026707106650,023414357153,020512567452,031232674755,032334044254,037027215557,034121425056,014667433341,017761203640,012472052143,011574662442,0254571745,03352341244,06041110547,05147720046,034461327270,037567517771,032674746072,031772176573,020052265674,023154455375,026247604476,025341034177,05407022260,06501612761,03612443062,0714273563,011034160664,012132750365,017221501466,014327331167,
+	031376553516,032270363017,037163132714,034065702215,025745411112,026643221413,023550070310,020456640611,0310656506,03216066007,06105237704,05003407205,014723714102,017625124403,012536375300,011430545601,020116142437,023010772136,026303523635,025205313334,034525000033,037423630532,032730461231,031636251730,011170247427,012076477126,017365626625,014263016324,05543305023,06445535522,03756764221,0650154720,
+	013637571754,010731341255,015422110556,016524720057,07204433350,04302203651,01011052152,02117662453,022651674744,021757044245,024444215546,027542425047,036262736340,035364106641,030077357142,033171567443,02457160675,01551750374,04642501477,07744331176,016064022271,015162612770,010271443073,013377273572,033431265665,030537455364,035624604467,036722034166,027002327261,024104517760,021217746063,022311176562,
+	}, { 00,0160465067,0341152156,0221537131,0702324334,0662741353,0443276262,0523613205,01604650670,01764235617,01545702726,01425367741,01106574544,01066111523,01247426412,01327043475,03411521560,03571144507,03750473436,03630016451,03313605654,03273260633,03052757702,03132332765,02215371310,02375714377,02154223246,02034646221,02517055024,02477430043,02656107172,02736562115,
+	07023243340,07143626327,07362311216,07202774271,07721167074,07641502013,07460035122,07500450145,06627413530,06747076557,06566541466,06406124401,06125737604,06045352663,06264665752,06304200735,04432762620,04552307647,04773630776,04613255711,04330446514,04250023573,04071514442,04111171425,05236132050,05356557037,05177060106,05017405161,05534216364,05454673303,05675344232,05715721255,
+	016046506700,016126163767,016307454656,016267031631,016744622434,016624247453,016405770562,016565315505,017642356170,017722733117,017503204026,017463661041,017140072244,017020417223,017201120312,017361545375,015457027260,015537442207,015716175336,015676510351,015355303154,015235766133,015014251002,015174634065,014253677410,014333212477,014112725546,014072340521,014551553724,014431136743,014610401672,014770064615,
+	011065745440,011105320427,011324617516,011244272571,011767461774,011607004713,011426533622,011546156645,010661115230,010701570257,010520047366,010440422301,010163231104,010003654163,010222363052,010342706035,012474264120,012514601147,012735336076,012655753011,012376140214,012216525273,012037012342,012157477325,013270434750,013310051737,013131566606,013051103661,013572710464,013412375403,013633642532,013753227555,
+	034115215600,034075670667,034254347756,034334722731,034617131534,034777554553,034556063462,034436406405,035711445070,035671020017,035450517126,035530172141,035013761344,035173304323,035352633212,035232256275,037504734360,037464351307,037645666236,037725203251,037206410054,037366075033,037147542102,037027127165,036300164510,036260501577,036041036446,036121453421,036402240624,036562625643,036743312772,036623777715,
+	033136056540,033056433527,033277104416,033317561471,033634372674,033754717613,033575220722,033415645745,032732606330,032652263357,032473754266,032513331201,032030522004,032150147063,032371470152,032211015135,030527577020,030447112047,030666425176,030706040111,030225653314,030345236373,030164701242,030004364225,031323327650,031243742637,031062275706,031102610761,031421003564,031541466503,031760151432,031600534455,
+	022153713100,022033376167,022212641056,022372224031,022651437234,022731052253,022510565362,022470100305,023757143770,023637526717,023416011626,023576474641,023055267444,023135602423,023314335512,023274750575,021542232460,021422657407,021603360536,021763705551,021240116754,021320573733,021101044602,021061421665,020346462210,020226007277,020007530346,020167155321,020444746124,020524323143,020705614072,020665271015,
+	025170550240,025010135227,025231402316,025351067371,025672674174,025712211113,025533726022,025453343045,024774300430,024614765457,024435252566,024555637501,024076024704,024116441763,024337176652,024257513635,026561071720,026401414747,026620123676,026740546611,026263355414,026303730473,026122207542,026042662525,027365621150,027205244137,027024773006,027144316061,027467505264,027507160203,027726457332,027646032355,
+	}, { 00,027057063545,025202344213,02255327756,021730513527,06767570062,04532657734,023565634271,030555024357,017502047612,015757360144,032700303401,011265537670,036232554335,034067673463,013030610126,012006253637,035051230372,037204117424,010253174161,033736740310,014761723655,016534404103,031563467446,022553277560,05504214025,07751133773,020706150236,03263764047,024234707502,026061420254,01036443711,
+	024014527476,03043544133,01216663665,026241600320,05724034151,022773057414,020526370342,07571313607,014541503721,033516560264,031743647532,016714624077,035271010206,012226073743,010073354015,037024337550,036012774241,011045717704,013210430052,034247453517,017722267766,030775204223,032520123575,015577140030,06547750116,021510733453,023745414305,04712477640,027277243431,0220220174,02075107622,025022164367,
+	023305054075,04352037530,06107310266,021150373723,02435547552,025462524017,027637603741,0660660204,013650070322,034607013667,036452334131,011405357474,032160563605,015137500340,017362627416,030335644153,031303207642,016354264307,014101143451,033156120114,010433714365,037464777620,035631450176,012666433433,01656223515,026601240050,024454167706,03403104243,020166730032,07131753577,05364474221,022333417764,
+	07311573403,020346510146,022113637610,05144654355,026421060124,01476003461,03623324337,024674347672,037644557754,010613534211,012446613547,035411670002,016174044273,031123027736,033376300060,014321363525,015317720234,032340743771,030115464027,017142407562,034427233713,013470250256,011625177500,036672114045,025642704163,02615767426,0440440370,027417423635,04172217444,023125274101,021370153657,06327130312,
+	035526333073,012571350536,010724077260,037773014725,014216620554,033241643011,031014564747,016043507202,05073317324,022024374661,020271053137,07226030472,024743604603,03714667346,01541540410,026516523155,027520160644,0577103301,02722224457,025775247112,06210473363,021247410626,023012737170,04045754435,017075144513,030022127056,032277200700,015220263245,036745457034,011712434571,013547713227,034510770762,
+	011532614405,036565677140,034730550616,013767533353,030202307122,017255364467,015000043331,032057020674,021067630752,06030653217,04265574541,023232517004,0757323275,027700340730,025555067066,02502004523,03534447232,024563424777,026736703021,01761760564,022204154715,05253137250,07006210506,020051273043,033061463165,014036400420,016263727376,031234744633,012751170442,035706113107,037553234651,010504257314,
+	016623367006,031674304543,033421023215,014476040750,037113674521,010144617064,012311530732,035346553277,026376343351,01321320614,03174007142,024123064407,07446650676,020411633333,022644514465,05613577120,04625134631,023672157374,021427270422,06470213167,025115427316,02142444653,0317763105,027340700440,034370110566,013327173023,011172254775,036125237230,015440403041,032417460504,030642747252,017615724717,
+	032637640470,015660623135,017435504663,030462567326,013107353157,034150330412,036305017344,011352074601,02362664727,025335607262,027160520534,0137543071,023452377200,04405314745,06650033013,021607050556,020631413247,07666470702,05433757054,022464734511,01101100760,026156163225,024303244573,03354227036,010364437110,037333454455,035166773303,012131710646,031454124437,016403147172,014656260624,033601203361,
+	} };
+
+	static uint32_t crc32_slice_by_4(const void* pData, size_t data_len, uint32_t cur_crc32 = 0)
+	{
+		uint32_t crc = ~cur_crc32;
+		const uint32_t* pData32 = static_cast<const uint32_t*>(pData);
+
+		for (; data_len >= sizeof(uint32_t); ++pData32, data_len -= 4)
+		{
+			uint32_t v = READ_LE32(pData32) ^ crc;
+			crc = g_crc32_4[0][v >> 24] ^ g_crc32_4[1][(v >> 16) & 0xFF] ^ g_crc32_4[2][(v >> 8) & 0xFF] ^ g_crc32_4[3][v & 0xFF];
+		}
+
+		for (const uint8_t* pData8 = reinterpret_cast<const uint8_t*>(pData32); data_len; --data_len)
+			crc = (crc >> 8) ^ g_crc32_4[0][(crc & 0xFF) ^ *pData8++];
+
+		return ~crc;
+	}
+
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE 
+	// See Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction":
+	// https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf
+	// Requires PCLMUL and SSE 4.1. This function skips Step 1 (fold by 4) for simplicity/less code.
+	static uint32_t crc32_pclmul(const uint8_t* p, size_t size, uint32_t crc)
+	{
+		assert(size >= 16);
+
+		// See page 22 (bit reflected constants for gzip)
+#ifdef _MSC_VER
+		static const uint64_t __declspec(align(16)) 
+#else
+		static const uint64_t __attribute__((aligned(16)))
+#endif
+			s_u[2] = { 0x1DB710641, 0x1F7011641 }, s_k5k0[2] = { 0x163CD6124, 0 }, s_k3k4[2] = { 0x1751997D0, 0xCCAA009E };
+
+		// Load first 16 bytes, apply initial CRC32
+		__m128i b = _mm_xor_si128(_mm_cvtsi32_si128(~crc), _mm_loadu_si128(reinterpret_cast<const __m128i*>(p)));
+
+		// We're skipping directly to Step 2 page 12 - iteratively folding by 1 (by 4 is overkill for our needs)
+		const __m128i k3k4 = _mm_load_si128(reinterpret_cast<const __m128i*>(s_k3k4));
+
+		for (size -= 16, p += 16; size >= 16; size -= 16, p += 16)
+			b = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(b, k3k4, 17), _mm_loadu_si128(reinterpret_cast<const __m128i*>(p))), _mm_clmulepi64_si128(b, k3k4, 0));
+
+		// Final stages: fold to 64-bits, 32-bit Barrett reduction
+		const __m128i z = _mm_set_epi32(0, ~0, 0, ~0), u = _mm_load_si128(reinterpret_cast<const __m128i*>(s_u));
+		b = _mm_xor_si128(_mm_srli_si128(b, 8), _mm_clmulepi64_si128(b, k3k4, 16));
+		b = _mm_xor_si128(_mm_clmulepi64_si128(_mm_and_si128(b, z), _mm_loadl_epi64(reinterpret_cast<const __m128i*>(s_k5k0)), 0), _mm_srli_si128(b, 4));
+		return ~_mm_extract_epi32(_mm_xor_si128(b, _mm_clmulepi64_si128(_mm_and_si128(_mm_clmulepi64_si128(_mm_and_si128(b, z), u, 16), z), u, 0)), 1);
+	}
+
+	static uint32_t crc32_sse41_simd(const unsigned char* buf, size_t len, uint32_t prev_crc32)
+	{
+		if (len < 16)
+			return crc32_slice_by_4(buf, len, prev_crc32);
+
+		uint32_t simd_len = len & ~15;
+		uint32_t c = crc32_pclmul(buf, simd_len, prev_crc32);
+		return crc32_slice_by_4(buf + simd_len, len - simd_len, c);
+	}
+#endif
+
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE 
+
+#ifndef _MSC_VER
+	static void do_cpuid(uint32_t eax, uint32_t ecx, uint32_t* regs)
+	{
+		uint32_t ebx = 0, edx = 0;
+
+#if defined(__PIC__) && defined(__i386__)
+		__asm__("movl %%ebx, %%edi;"
+			"cpuid;"
+			"xchgl %%ebx, %%edi;"
+			: "=D"(ebx), "+a"(eax), "+c"(ecx), "=d"(edx));
+#else
+		__asm__("cpuid;" : "+b"(ebx), "+a"(eax), "+c"(ecx), "=d"(edx));
+#endif
+
+		regs[0] = eax; regs[1] = ebx; regs[2] = ecx; regs[3] = edx;
+	}
+#endif
+
+	struct cpu_info
+	{
+		cpu_info() { memset(this, 0, sizeof(*this)); }
+
+		bool m_initialized, m_has_fpu, m_has_mmx, m_has_sse, m_has_sse2, m_has_sse3, m_has_ssse3, m_has_sse41, m_has_sse42, m_has_avx, m_has_avx2, m_has_pclmulqdq;
+				
+		void init()
+		{
+			if (m_initialized)
+				return;
+
+			int regs[4];
+
+#ifdef _MSC_VER
+			__cpuid(regs, 0);
+#else
+			do_cpuid(0, 0, (uint32_t*)regs);
+#endif
+
+			const uint32_t max_eax = regs[0];
+			if (max_eax >= 1U)
+			{
+#ifdef _MSC_VER
+				__cpuid(regs, 1);
+#else
+				do_cpuid(1, 0, (uint32_t*)regs);
+#endif
+				extract_x86_flags(regs[2], regs[3]);
+			}
+
+			if (max_eax >= 7U)
+			{
+#ifdef _MSC_VER
+				__cpuidex(regs, 7, 0);
+#else
+				do_cpuid(7, 0, (uint32_t*)regs);
+#endif
+				extract_x86_extended_flags(regs[1]);
+			}
+
+			m_initialized = true;
+		}
+
+		bool can_use_sse41() const { return m_has_sse && m_has_sse2 && m_has_sse3 && m_has_ssse3 && m_has_sse41; }
+		bool can_use_pclmul() const	{ return m_has_pclmulqdq && can_use_sse41(); }
+
+	private:
+		void extract_x86_flags(uint32_t ecx, uint32_t edx)
+		{
+			m_has_fpu = (edx & (1 << 0)) != 0;	m_has_mmx = (edx & (1 << 23)) != 0;	m_has_sse = (edx & (1 << 25)) != 0; m_has_sse2 = (edx & (1 << 26)) != 0;
+			m_has_sse3 = (ecx & (1 << 0)) != 0; m_has_ssse3 = (ecx & (1 << 9)) != 0; m_has_sse41 = (ecx & (1 << 19)) != 0; m_has_sse42 = (ecx & (1 << 20)) != 0;
+			m_has_pclmulqdq = (ecx & (1 << 1)) != 0; m_has_avx = (ecx & (1 << 28)) != 0;
+		}
+
+		void extract_x86_extended_flags(uint32_t ebx) { m_has_avx2 = (ebx & (1 << 5)) != 0; }
+	};
+
+	cpu_info g_cpu_info;
+		
+	void fpng_init()
+	{
+		g_cpu_info.init();
+	}
+#else
+	void fpng_init()
+	{
+	}
+#endif
+
+	bool fpng_cpu_supports_sse41()
+	{
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE 
+		assert(g_cpu_info.m_initialized);
+		return g_cpu_info.can_use_sse41();
+#else
+		return false;
+#endif
+	}
+
+	uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32)
+	{
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE 
+		if (g_cpu_info.can_use_pclmul())
+			return crc32_sse41_simd(static_cast<const uint8_t *>(pData), size, prev_crc32);
+#endif
+
+		return crc32_slice_by_4(pData, size, prev_crc32);
+	}
+
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE 
+	// See "Fast Computation of Adler32 Checksums":
+	// https://www.intel.com/content/www/us/en/developer/articles/technical/fast-computation-of-adler32-checksums.html
+	// SSE 4.1, 16 bytes per iteration
+	static uint32_t adler32_sse_16(const uint8_t* p, size_t len, uint32_t initial)
+	{
+		uint32_t s1 = initial & 0xFFFF, s2 = initial >> 16;
+		const uint32_t K = 65521;
+
+		while (len >= 16)
+		{
+			__m128i a = _mm_setr_epi32(s1, 0, 0, 0), b = _mm_setzero_si128(), c = _mm_setzero_si128(), d = _mm_setzero_si128(), 
+				e = _mm_setzero_si128(), f = _mm_setzero_si128(), g = _mm_setzero_si128(), h = _mm_setzero_si128();
+
+			const size_t n = minimum<size_t>(len >> 4, 5552);
+
+			for (size_t i = 0; i < n; i++)
+			{
+				const __m128i v = _mm_loadu_si128((const __m128i*)(p + i * 16));
+				a = _mm_add_epi32(a, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(0, 0, 0, 0)))); b = _mm_add_epi32(b, a);
+				c = _mm_add_epi32(c, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(1, 1, 1, 1)))); d = _mm_add_epi32(d, c);
+				e = _mm_add_epi32(e, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(2, 2, 2, 2)))); f = _mm_add_epi32(f, e);
+				g = _mm_add_epi32(g, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(3, 3, 3, 3)))); h = _mm_add_epi32(h, g);
+			}
+
+			uint32_t sa[16], sb[16];
+			_mm_storeu_si128((__m128i*)sa, a); _mm_storeu_si128((__m128i*)(sa + 4), c);
+			_mm_storeu_si128((__m128i*)sb, b); _mm_storeu_si128((__m128i*)(sb + 4), d);
+			_mm_storeu_si128((__m128i*)(sa + 8), e); _mm_storeu_si128((__m128i*)(sa + 12), g);
+			_mm_storeu_si128((__m128i*)(sb + 8), f); _mm_storeu_si128((__m128i*)(sb + 12), h);
+
+			// This could be vectorized, but it's only executed every 5552*16 iterations.
+			uint64_t vs1 = 0;
+			for (uint32_t i = 0; i < 16; i++)
+				vs1 += sa[i];
+
+			uint64_t vs2_a = 0;
+			for (uint32_t i = 0; i < 16; i++)
+				vs2_a += sa[i] * (uint64_t)i;
+			uint64_t vs2_b = 0;
+			for (uint32_t i = 0; i < 16; i++)
+				vs2_b += sb[i];
+			vs2_b *= 16U;
+			uint64_t vs2 = vs2_b - vs2_a + s2;
+
+			s1 = (uint32_t)(vs1 % K);
+			s2 = (uint32_t)(vs2 % K);
+
+			p += n * 16;
+			len -= n * 16;
+		}
+
+		for (; len; len--)
+		{
+			s1 += *p++;
+			s2 += s1;
+		}
+
+		return (s1 % K) | ((s2 % K) << 16);
+	}
+#endif
+
+	static uint32_t fpng_adler32_scalar(const uint8_t* ptr, size_t buf_len, uint32_t adler)
+	{
+		uint32_t i, s1 = (uint32_t)(adler & 0xffff), s2 = (uint32_t)(adler >> 16); uint32_t block_len = (uint32_t)(buf_len % 5552);
+		if (!ptr) return FPNG_ADLER32_INIT;
+		while (buf_len) {
+			for (i = 0; i + 7 < block_len; i += 8, ptr += 8) {
+				s1 += ptr[0], s2 += s1; s1 += ptr[1], s2 += s1; s1 += ptr[2], s2 += s1; s1 += ptr[3], s2 += s1;
+				s1 += ptr[4], s2 += s1; s1 += ptr[5], s2 += s1; s1 += ptr[6], s2 += s1; s1 += ptr[7], s2 += s1;
+			}
+			for (; i < block_len; ++i) s1 += *ptr++, s2 += s1;
+			s1 %= 65521U, s2 %= 65521U; buf_len -= block_len; block_len = 5552;
+		}
+		return (s2 << 16) + s1;
+	}
+
+	uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler)
+	{
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE 
+		if (g_cpu_info.can_use_sse41())
+			return adler32_sse_16((const uint8_t*)pData, size, adler);
+#endif
+		return fpng_adler32_scalar((const uint8_t*)pData, size, adler);
+	}
+
+	// Ensure we've been configured for endianness correctly.
+	static inline bool endian_check()
+	{
+		uint32_t endian_check = 0;
+		WRITE_LE32(&endian_check, 0x1234ABCD);
+		const uint32_t first_byte = reinterpret_cast<const uint8_t*>(&endian_check)[0];
+		return first_byte == 0xCD;
+	}
+		
+	static const uint16_t g_defl_len_sym[256] = {
+	  257,258,259,260,261,262,263,264,265,265,266,266,267,267,268,268,269,269,269,269,270,270,270,270,271,271,271,271,272,272,272,272,
+	  273,273,273,273,273,273,273,273,274,274,274,274,274,274,274,274,275,275,275,275,275,275,275,275,276,276,276,276,276,276,276,276,
+	  277,277,277,277,277,277,277,277,277,277,277,277,277,277,277,277,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,
+	  279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,280,280,280,280,280,280,280,280,280,280,280,280,280,280,280,280,
+	  281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,
+	  282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,
+	  283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,
+	  284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,285 };
+
+	static const uint8_t g_defl_len_extra[256] = {
+	  0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
+	  4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
+	  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
+	  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,0 };
+
+	static const uint8_t g_defl_small_dist_sym[512] = {
+	  0,1,2,3,4,4,5,5,6,6,6,6,7,7,7,7,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,
+	  11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13,
+	  13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,14,
+	  14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,
+	  14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,
+	  15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16,
+	  16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+	  16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+	  16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+	  17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+	  17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+	  17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17 };
+		
+	static const uint32_t g_bitmasks[17] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
+
+	// Huffman tables generated by fpng_test -t @filelist.txt. Total alpha files : 1440, Total opaque files : 5627.
+	// Feel free to retrain the encoder on your opaque/alpha PNG files by setting FPNG_TRAIN_HUFFMAN_TABLES and running fpng_test with the -t option.
+	static const uint8_t g_dyn_huff_3[] = {
+	120, 1, 237, 195, 3, 176, 110, 89, 122, 128, 225, 247, 251, 214, 218, 248, 113, 124, 173, 190, 109, 12, 50, 201, 196, 182, 109, 219, 182, 109, 219, 182,
+	109, 219, 201, 36, 147, 153, 105, 235, 246, 53, 142, 207, 143, 141, 181, 214, 151, 93, 117, 170, 78, 117, 117, 58, 206, 77, 210, 217, 169, 122 };
+	const uint32_t DYN_HUFF_3_BITBUF = 30, DYN_HUFF_3_BITBUF_SIZE = 7;
+	static const struct { uint8_t m_code_size; uint16_t m_code; } g_dyn_huff_3_codes[288] = {
+	{2,0},{4,2},{4,10},{5,14},{5,30},{6,25},{6,57},{6,5},{6,37},{7,3},{7,67},{7,35},{7,99},{8,11},{8,139},{8,75},{8,203},{8,43},{8,171},{8,107},{9,135},{9,391},{9,71},{9,327},{9,199},{9,455},{9,39},{9,295},{9,167},{9,423},{9,103},{10,183},
+	{9,359},{10,695},{10,439},{10,951},{10,119},{10,631},{10,375},{10,887},{10,247},{10,759},{10,503},{11,975},{11,1999},{11,47},{11,1071},{12,1199},{11,559},{12,3247},{12,687},{11,1583},{12,2735},{12,1711},{12,3759},{12,431},{12,2479},{12,1455},{12,3503},{12,943},{12,2991},{12,1967},{12,4015},{12,111},
+	{12,2159},{12,1135},{12,3183},{12,623},{12,2671},{12,1647},{12,3695},{12,367},{12,2415},{12,1391},{12,3439},{12,879},{12,2927},{12,1903},{12,3951},{12,239},{12,2287},{12,1263},{12,3311},{12,751},{12,2799},{12,1775},{12,3823},{12,495},{12,2543},{12,1519},{12,3567},{12,1007},{12,3055},{12,2031},{12,4079},{12,31},
+	{12,2079},{12,1055},{12,3103},{12,543},{12,2591},{12,1567},{12,3615},{12,287},{12,2335},{12,1311},{12,3359},{12,799},{12,2847},{12,1823},{12,3871},{12,159},{12,2207},{12,1183},{12,3231},{12,671},{12,2719},{12,1695},{12,3743},{12,415},{12,2463},{12,1439},{12,3487},{12,927},{12,2975},{12,1951},{12,3999},{12,95},
+	{12,2143},{12,1119},{12,3167},{12,607},{12,2655},{12,1631},{12,3679},{12,351},{12,2399},{12,1375},{12,3423},{12,863},{12,2911},{12,1887},{12,3935},{12,223},{12,2271},{12,1247},{12,3295},{12,735},{12,2783},{12,1759},{12,3807},{12,479},{12,2527},{12,1503},{12,3551},{12,991},{12,3039},{12,2015},{12,4063},{12,63},
+	{12,2111},{12,1087},{12,3135},{12,575},{12,2623},{12,1599},{12,3647},{12,319},{12,2367},{12,1343},{12,3391},{12,831},{12,2879},{12,1855},{12,3903},{12,191},{12,2239},{12,1215},{12,3263},{12,703},{12,2751},{12,1727},{12,3775},{12,447},{12,2495},{12,1471},{12,3519},{12,959},{12,3007},{12,1983},{12,4031},{12,127},
+	{12,2175},{12,1151},{12,3199},{12,639},{12,2687},{12,1663},{12,3711},{12,383},{12,2431},{12,1407},{12,3455},{12,895},{12,2943},{11,303},{12,1919},{12,3967},{11,1327},{12,255},{11,815},{11,1839},{11,175},{10,1015},{10,15},{10,527},{10,271},{10,783},{10,143},{10,655},{10,399},{10,911},{10,79},{10,591},
+	{9,231},{10,335},{9,487},{9,23},{9,279},{9,151},{9,407},{9,87},{9,343},{9,215},{9,471},{9,55},{8,235},{8,27},{8,155},{8,91},{8,219},{8,59},{8,187},{8,123},{7,19},{7,83},{7,51},{7,115},{6,21},{6,53},{6,13},{6,45},{5,1},{5,17},{5,9},{4,6},
+	{12,2303},{6,29},{0,0},{0,0},{8,251},{0,0},{0,0},{8,7},{0,0},{10,847},{0,0},{10,207},{12,1279},{10,719},{12,3327},{12,767},{12,2815},{12,1791},{12,3839},{12,511},{12,2559},{12,1535},{9,311},{12,3583},{12,1023},{12,3071},{10,463},{12,2047},{6,61},{12,4095},{0,0},{0,0}
+	};
+
+	static const uint8_t g_dyn_huff_4[] = {
+	120, 1, 229, 196, 99, 180, 37, 103, 218, 128, 225, 251, 121, 171, 106, 243, 216, 231, 180, 109, 196, 182, 51, 51, 73, 6, 201, 216, 182, 109, 219, 182,
+	17, 140, 98, 219, 102, 219, 60, 125, 172, 205, 170, 122, 159, 111, 213, 143, 179, 214, 94, 189, 58, 153, 104, 166, 103, 190, 247, 199, 117 };
+	const uint32_t DYN_HUFF_4_BITBUF = 1, DYN_HUFF_4_BITBUF_SIZE = 2;
+	static const struct { uint8_t m_code_size; uint16_t m_code; } g_dyn_huff_4_codes[288] = {
+	{2,0},{4,2},{5,6},{6,30},{6,62},{6,1},{7,41},{7,105},{7,25},{7,89},{7,57},{7,121},{8,117},{8,245},{8,13},{8,141},{8,77},{8,205},{8,45},{8,173},{8,109},{8,237},{8,29},{8,157},{8,93},{8,221},{8,61},{9,83},{9,339},{9,211},{9,467},{9,51},
+	{9,307},{9,179},{9,435},{9,115},{9,371},{9,243},{9,499},{9,11},{9,267},{9,139},{9,395},{9,75},{9,331},{9,203},{9,459},{9,43},{9,299},{10,7},{10,519},{10,263},{10,775},{10,135},{10,647},{10,391},{10,903},{10,71},{10,583},{10,327},{10,839},{10,199},{10,711},{10,455},
+	{10,967},{10,39},{10,551},{10,295},{10,807},{10,167},{10,679},{10,423},{10,935},{10,103},{10,615},{11,463},{11,1487},{11,975},{10,359},{10,871},{10,231},{11,1999},{11,47},{11,1071},{11,559},{10,743},{10,487},{11,1583},{11,303},{11,1327},{11,815},{11,1839},{11,175},{11,1199},{11,687},{11,1711},
+	{11,431},{11,1455},{11,943},{11,1967},{11,111},{11,1135},{11,623},{11,1647},{11,367},{11,1391},{11,879},{11,1903},{11,239},{11,1263},{11,751},{11,1775},{11,495},{11,1519},{11,1007},{11,2031},{11,31},{11,1055},{11,543},{11,1567},{11,287},{11,1311},{11,799},{11,1823},{11,159},{11,1183},{11,671},{11,1695},
+	{11,415},{11,1439},{11,927},{11,1951},{11,95},{11,1119},{11,607},{11,1631},{11,351},{11,1375},{11,863},{11,1887},{11,223},{11,1247},{11,735},{11,1759},{11,479},{11,1503},{11,991},{11,2015},{11,63},{11,1087},{11,575},{11,1599},{11,319},{11,1343},{11,831},{11,1855},{11,191},{11,1215},{11,703},{11,1727},
+	{11,447},{11,1471},{11,959},{11,1983},{11,127},{11,1151},{11,639},{11,1663},{11,383},{10,999},{10,23},{10,535},{10,279},{11,1407},{11,895},{11,1919},{11,255},{11,1279},{10,791},{10,151},{10,663},{10,407},{10,919},{10,87},{10,599},{10,343},{10,855},{10,215},{10,727},{10,471},{10,983},{10,55},
+	{10,567},{10,311},{10,823},{10,183},{10,695},{10,439},{10,951},{10,119},{10,631},{10,375},{10,887},{10,247},{10,759},{10,503},{10,1015},{10,15},{10,527},{10,271},{10,783},{10,143},{10,655},{10,399},{9,171},{9,427},{9,107},{9,363},{9,235},{9,491},{9,27},{9,283},{9,155},{9,411},
+	{9,91},{9,347},{9,219},{9,475},{9,59},{9,315},{9,187},{9,443},{8,189},{9,123},{8,125},{8,253},{8,3},{8,131},{8,67},{8,195},{8,35},{8,163},{8,99},{8,227},{8,19},{7,5},{7,69},{7,37},{7,101},{7,21},{7,85},{6,33},{6,17},{6,49},{5,22},{4,10},
+	{12,2047},{0,0},{6,9},{0,0},{0,0},{0,0},{8,147},{0,0},{0,0},{7,53},{0,0},{9,379},{0,0},{9,251},{10,911},{10,79},{11,767},{10,591},{10,335},{10,847},{10,207},{10,719},{11,1791},{11,511},{9,507},{11,1535},{11,1023},{12,4095},{5,14},{0,0},{0,0},{0,0}
+	};
+
+#define PUT_BITS(bb, ll) do { uint32_t b = bb, l = ll; assert((l) >= 0 && (l) <= 16); assert((b) < (1ULL << (l))); bit_buf |= (((uint64_t)(b)) << bit_buf_size); bit_buf_size += (l); assert(bit_buf_size <= 64); } while(0)
+#define PUT_BITS_CZ(bb, ll) do { uint32_t b = bb, l = ll; assert((l) >= 1 && (l) <= 16); assert((b) < (1ULL << (l))); bit_buf |= (((uint64_t)(b)) << bit_buf_size); bit_buf_size += (l); assert(bit_buf_size <= 64); } while(0)
+
+#define PUT_BITS_FLUSH do { \
+	if ((dst_ofs + 8) > dst_buf_size) \
+		return 0; \
+	WRITE_LE64(pDst + dst_ofs, bit_buf); \
+	uint32_t bits_to_shift = bit_buf_size & ~7; \
+	dst_ofs += (bits_to_shift >> 3); \
+	assert(bits_to_shift < 64); \
+	bit_buf = bit_buf >> bits_to_shift; \
+	bit_buf_size -= bits_to_shift; \
+} while(0)
+
+#define PUT_BITS_FORCE_FLUSH do { \
+	while (bit_buf_size > 0) \
+	{ \
+		if ((dst_ofs + 1) > dst_buf_size) \
+			return 0; \
+		*(uint8_t*)(pDst + dst_ofs) = (uint8_t)bit_buf; \
+		dst_ofs++; \
+		bit_buf >>= 8; \
+		bit_buf_size -= 8; \
+	} \
+} while(0)
+
+	enum
+	{
+		DEFL_MAX_HUFF_TABLES = 3,
+		DEFL_MAX_HUFF_SYMBOLS = 288,	
+		DEFL_MAX_HUFF_SYMBOLS_0 = 288,	
+		DEFL_MAX_HUFF_SYMBOLS_1 = 32,
+		DEFL_MAX_HUFF_SYMBOLS_2 = 19,
+		DEFL_LZ_DICT_SIZE = 32768,
+		DEFL_LZ_DICT_SIZE_MASK = DEFL_LZ_DICT_SIZE - 1,
+		DEFL_MIN_MATCH_LEN = 3,
+		DEFL_MAX_MATCH_LEN = 258
+	};
+
+#if FPNG_TRAIN_HUFFMAN_TABLES
+	uint64_t g_huff_counts[HUFF_COUNTS_SIZE];
+#endif
+
+	struct defl_huff
+	{
+		uint16_t m_huff_count[DEFL_MAX_HUFF_TABLES][DEFL_MAX_HUFF_SYMBOLS];
+		uint16_t m_huff_codes[DEFL_MAX_HUFF_TABLES][DEFL_MAX_HUFF_SYMBOLS];
+		uint8_t m_huff_code_sizes[DEFL_MAX_HUFF_TABLES][DEFL_MAX_HUFF_SYMBOLS];
+	};
+
+	struct defl_sym_freq
+	{
+		uint16_t m_key;
+		uint16_t m_sym_index;
+	};
+
+#define DEFL_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj))
+
+	static defl_sym_freq* defl_radix_sort_syms(uint32_t num_syms, defl_sym_freq* pSyms0, defl_sym_freq* pSyms1)
+	{
+		uint32_t total_passes = 2, pass_shift, pass, i, hist[256 * 2]; defl_sym_freq* pCur_syms = pSyms0, * pNew_syms = pSyms1; DEFL_CLEAR_OBJ(hist);
+		for (i = 0; i < num_syms; i++) { uint32_t freq = pSyms0[i].m_key; hist[freq & 0xFF]++; hist[256 + ((freq >> 8) & 0xFF)]++; }
+		while ((total_passes > 1) && (num_syms == hist[(total_passes - 1) * 256])) total_passes--;
+		for (pass_shift = 0, pass = 0; pass < total_passes; pass++, pass_shift += 8)
+		{
+			const uint32_t* pHist = &hist[pass << 8];
+			uint32_t offsets[256], cur_ofs = 0;
+			for (i = 0; i < 256; i++) { offsets[i] = cur_ofs; cur_ofs += pHist[i]; }
+			for (i = 0; i < num_syms; i++) pNew_syms[offsets[(pCur_syms[i].m_key >> pass_shift) & 0xFF]++] = pCur_syms[i];
+			{ defl_sym_freq* t = pCur_syms; pCur_syms = pNew_syms; pNew_syms = t; }
+		}
+		return pCur_syms;
+	}
+
+	// defl_calculate_minimum_redundancy() originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996.
+	static void defl_calculate_minimum_redundancy(defl_sym_freq* A, int n)
+	{
+		int root, leaf, next, avbl, used, dpth;
+		if (n == 0) return; else if (n == 1) { A[0].m_key = 1; return; }
+		A[0].m_key += A[1].m_key; root = 0; leaf = 2;
+		for (next = 1; next < n - 1; next++)
+		{
+			if (leaf >= n || A[root].m_key < A[leaf].m_key) { A[next].m_key = A[root].m_key; A[root++].m_key = (uint16_t)next; }
+			else A[next].m_key = A[leaf++].m_key;
+			if (leaf >= n || (root < next && A[root].m_key < A[leaf].m_key)) { A[next].m_key = (uint16_t)(A[next].m_key + A[root].m_key); A[root++].m_key = (uint16_t)next; }
+			else A[next].m_key = (uint16_t)(A[next].m_key + A[leaf++].m_key);
+		}
+		A[n - 2].m_key = 0; for (next = n - 3; next >= 0; next--) A[next].m_key = A[A[next].m_key].m_key + 1;
+		avbl = 1; used = dpth = 0; root = n - 2; next = n - 1;
+		while (avbl > 0)
+		{
+			while (root >= 0 && (int)A[root].m_key == dpth) { used++; root--; }
+			while (avbl > used) { A[next--].m_key = (uint16_t)(dpth); avbl--; }
+			avbl = 2 * used; dpth++; used = 0;
+		}
+	}
+
+	// Limits canonical Huffman code table's max code size.
+	enum { DEFL_MAX_SUPPORTED_HUFF_CODESIZE = 32 };
+	static void defl_huffman_enforce_max_code_size(int* pNum_codes, int code_list_len, int max_code_size)
+	{
+		int i; uint32_t total = 0; if (code_list_len <= 1) return;
+		for (i = max_code_size + 1; i <= DEFL_MAX_SUPPORTED_HUFF_CODESIZE; i++) pNum_codes[max_code_size] += pNum_codes[i];
+		for (i = max_code_size; i > 0; i--) total += (((uint32_t)pNum_codes[i]) << (max_code_size - i));
+		while (total != (1UL << max_code_size))
+		{
+			pNum_codes[max_code_size]--;
+			for (i = max_code_size - 1; i > 0; i--) if (pNum_codes[i]) { pNum_codes[i]--; pNum_codes[i + 1] += 2; break; }
+			total--;
+		}
+	}
+
+	static void defl_optimize_huffman_table(defl_huff* d, int table_num, int table_len, int code_size_limit, int static_table)
+	{
+		int i, j, l, num_codes[1 + DEFL_MAX_SUPPORTED_HUFF_CODESIZE]; uint32_t next_code[DEFL_MAX_SUPPORTED_HUFF_CODESIZE + 1]; DEFL_CLEAR_OBJ(num_codes);
+		if (static_table)
+		{
+			for (i = 0; i < table_len; i++) num_codes[d->m_huff_code_sizes[table_num][i]]++;
+		}
+		else
+		{
+			defl_sym_freq syms0[DEFL_MAX_HUFF_SYMBOLS], syms1[DEFL_MAX_HUFF_SYMBOLS], * pSyms;
+			int num_used_syms = 0;
+			const uint16_t* pSym_count = &d->m_huff_count[table_num][0];
+			for (i = 0; i < table_len; i++) if (pSym_count[i]) { syms0[num_used_syms].m_key = (uint16_t)pSym_count[i]; syms0[num_used_syms++].m_sym_index = (uint16_t)i; }
+
+			pSyms = defl_radix_sort_syms(num_used_syms, syms0, syms1); defl_calculate_minimum_redundancy(pSyms, num_used_syms);
+
+			for (i = 0; i < num_used_syms; i++) num_codes[pSyms[i].m_key]++;
+
+			defl_huffman_enforce_max_code_size(num_codes, num_used_syms, code_size_limit);
+
+			DEFL_CLEAR_OBJ(d->m_huff_code_sizes[table_num]); DEFL_CLEAR_OBJ(d->m_huff_codes[table_num]);
+			for (i = 1, j = num_used_syms; i <= code_size_limit; i++)
+				for (l = num_codes[i]; l > 0; l--) d->m_huff_code_sizes[table_num][pSyms[--j].m_sym_index] = (uint8_t)(i);
+		}
+
+		next_code[1] = 0; for (j = 0, i = 2; i <= code_size_limit; i++) next_code[i] = j = ((j + num_codes[i - 1]) << 1);
+
+		for (i = 0; i < table_len; i++)
+		{
+			uint32_t rev_code = 0, code, code_size; if ((code_size = d->m_huff_code_sizes[table_num][i]) == 0) continue;
+			code = next_code[code_size]++; for (l = code_size; l > 0; l--, code >>= 1) rev_code = (rev_code << 1) | (code & 1);
+			d->m_huff_codes[table_num][i] = (uint16_t)rev_code;
+		}
+	}
+
+#define DEFL_RLE_PREV_CODE_SIZE() { if (rle_repeat_count) { \
+  if (rle_repeat_count < 3) { \
+    d->m_huff_count[2][prev_code_size] = (uint16_t)(d->m_huff_count[2][prev_code_size] + rle_repeat_count); \
+    while (rle_repeat_count--) packed_code_sizes[num_packed_code_sizes++] = prev_code_size; \
+  } else { \
+    d->m_huff_count[2][16] = (uint16_t)(d->m_huff_count[2][16] + 1); packed_code_sizes[num_packed_code_sizes++] = 16; packed_code_sizes[num_packed_code_sizes++] = (uint8_t)(rle_repeat_count - 3); \
+} rle_repeat_count = 0; } }
+
+#define DEFL_RLE_ZERO_CODE_SIZE() { if (rle_z_count) { \
+  if (rle_z_count < 3) { \
+    d->m_huff_count[2][0] = (uint16_t)(d->m_huff_count[2][0] + rle_z_count); while (rle_z_count--) packed_code_sizes[num_packed_code_sizes++] = 0; \
+  } else if (rle_z_count <= 10) { \
+    d->m_huff_count[2][17] = (uint16_t)(d->m_huff_count[2][17] + 1); packed_code_sizes[num_packed_code_sizes++] = 17; packed_code_sizes[num_packed_code_sizes++] = (uint8_t)(rle_z_count - 3); \
+  } else { \
+    d->m_huff_count[2][18] = (uint16_t)(d->m_huff_count[2][18] + 1); packed_code_sizes[num_packed_code_sizes++] = 18; packed_code_sizes[num_packed_code_sizes++] = (uint8_t)(rle_z_count - 11); \
+} rle_z_count = 0; } }
+
+	static uint8_t g_defl_packed_code_size_syms_swizzle[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
+
+#define DEFL_DYN_PUT_BITS(bb, ll) \
+do { \
+	uint32_t b = (bb), l = (ll); \
+	assert((l) >= 1 && (l) <= 16); assert((b) < (1ULL << (l))); \
+	bit_buf |= (((uint64_t)(b)) << bit_buf_size); bit_buf_size += (l); assert(bit_buf_size <= 64); \
+	while (bit_buf_size >= 8) \
+	{ \
+		if ((dst_ofs + 1) > dst_buf_size) \
+			return false; \
+		*(uint8_t*)(pDst + dst_ofs) = (uint8_t)bit_buf; \
+		dst_ofs++; \
+		bit_buf >>= 8; \
+		bit_buf_size -= 8; \
+	} \
+} while(0)
+
+	static bool defl_start_dynamic_block(defl_huff* d, uint8_t* pDst, uint32_t& dst_ofs, uint32_t dst_buf_size, uint64_t& bit_buf, int& bit_buf_size)
+	{
+		int num_lit_codes, num_dist_codes, num_bit_lengths; uint32_t i, total_code_sizes_to_pack, num_packed_code_sizes, rle_z_count, rle_repeat_count, packed_code_sizes_index;
+		uint8_t code_sizes_to_pack[DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1], packed_code_sizes[DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1], prev_code_size = 0xFF;
+
+#if FPNG_TRAIN_HUFFMAN_TABLES
+		assert(HUFF_COUNTS_SIZE == DEFL_MAX_HUFF_SYMBOLS_0);
+		for (uint32_t i = 0; i < DEFL_MAX_HUFF_SYMBOLS_0; i++)
+			g_huff_counts[i] += d->m_huff_count[0][i];
+#endif
+
+		d->m_huff_count[0][256] = 1;
+
+		defl_optimize_huffman_table(d, 0, DEFL_MAX_HUFF_SYMBOLS_0, 12, FPNG_FALSE);
+		defl_optimize_huffman_table(d, 1, DEFL_MAX_HUFF_SYMBOLS_1, 12, FPNG_FALSE);
+
+		for (num_lit_codes = 286; num_lit_codes > 257; num_lit_codes--) if (d->m_huff_code_sizes[0][num_lit_codes - 1]) break;
+		for (num_dist_codes = 30; num_dist_codes > 1; num_dist_codes--) if (d->m_huff_code_sizes[1][num_dist_codes - 1]) break;
+
+		memcpy(code_sizes_to_pack, &d->m_huff_code_sizes[0][0], num_lit_codes);
+		memcpy(code_sizes_to_pack + num_lit_codes, &d->m_huff_code_sizes[1][0], num_dist_codes);
+		total_code_sizes_to_pack = num_lit_codes + num_dist_codes; num_packed_code_sizes = 0; rle_z_count = 0; rle_repeat_count = 0;
+
+		memset(&d->m_huff_count[2][0], 0, sizeof(d->m_huff_count[2][0]) * DEFL_MAX_HUFF_SYMBOLS_2);
+		for (i = 0; i < total_code_sizes_to_pack; i++)
+		{
+			uint8_t code_size = code_sizes_to_pack[i];
+			if (!code_size)
+			{
+				DEFL_RLE_PREV_CODE_SIZE();
+				if (++rle_z_count == 138) { DEFL_RLE_ZERO_CODE_SIZE(); }
+			}
+			else
+			{
+				DEFL_RLE_ZERO_CODE_SIZE();
+				if (code_size != prev_code_size)
+				{
+					DEFL_RLE_PREV_CODE_SIZE();
+					d->m_huff_count[2][code_size] = (uint16_t)(d->m_huff_count[2][code_size] + 1); packed_code_sizes[num_packed_code_sizes++] = code_size;
+				}
+				else if (++rle_repeat_count == 6)
+				{
+					DEFL_RLE_PREV_CODE_SIZE();
+				}
+			}
+			prev_code_size = code_size;
+		}
+		if (rle_repeat_count) { DEFL_RLE_PREV_CODE_SIZE(); }
+		else { DEFL_RLE_ZERO_CODE_SIZE(); }
+
+		defl_optimize_huffman_table(d, 2, DEFL_MAX_HUFF_SYMBOLS_2, 7, FPNG_FALSE);
+
+		// max of 2+5+5+4+18*3+(288+32)*7=2310 bits
+		DEFL_DYN_PUT_BITS(2, 2);
+
+		DEFL_DYN_PUT_BITS(num_lit_codes - 257, 5);
+		DEFL_DYN_PUT_BITS(num_dist_codes - 1, 5);
+
+		for (num_bit_lengths = 18; num_bit_lengths >= 0; num_bit_lengths--) if (d->m_huff_code_sizes[2][g_defl_packed_code_size_syms_swizzle[num_bit_lengths]]) break;
+		num_bit_lengths = maximum<int>(4, (num_bit_lengths + 1)); DEFL_DYN_PUT_BITS(num_bit_lengths - 4, 4);
+		for (i = 0; (int)i < num_bit_lengths; i++) DEFL_DYN_PUT_BITS(d->m_huff_code_sizes[2][g_defl_packed_code_size_syms_swizzle[i]], 3);
+
+		for (packed_code_sizes_index = 0; packed_code_sizes_index < num_packed_code_sizes; )
+		{
+			uint32_t code = packed_code_sizes[packed_code_sizes_index++]; assert(code < DEFL_MAX_HUFF_SYMBOLS_2);
+			DEFL_DYN_PUT_BITS(d->m_huff_codes[2][code], d->m_huff_code_sizes[2][code]);
+			if (code >= 16) DEFL_DYN_PUT_BITS(packed_code_sizes[packed_code_sizes_index++], "\02\03\07"[code - 16]);
+		}
+
+		return true;
+	}
+
+	static uint32_t write_raw_block(const uint8_t* pSrc, uint32_t src_len, uint8_t* pDst, uint32_t dst_buf_size)
+	{
+		if (dst_buf_size < 2)
+			return 0;
+
+		pDst[0] = 0x78;
+		pDst[1] = 0x01;
+
+		uint32_t dst_ofs = 2;
+
+		uint32_t src_ofs = 0;
+		while (src_ofs < src_len)
+		{
+			const uint32_t src_remaining = src_len - src_ofs;
+			const uint32_t block_size = minimum<uint32_t>(UINT16_MAX, src_remaining);
+			const bool final_block = (block_size == src_remaining);
+
+			if ((dst_ofs + 5 + block_size) > dst_buf_size)
+				return 0;
+
+			pDst[dst_ofs + 0] = final_block ? 1 : 0;
+
+			pDst[dst_ofs + 1] = block_size & 0xFF;
+			pDst[dst_ofs + 2] = (block_size >> 8) & 0xFF;
+
+			pDst[dst_ofs + 3] = (~block_size) & 0xFF;
+			pDst[dst_ofs + 4] = ((~block_size) >> 8) & 0xFF;
+
+			memcpy(pDst + dst_ofs + 5, pSrc + src_ofs, block_size);
+
+			src_ofs += block_size;
+			dst_ofs += 5 + block_size;
+		}
+
+		uint32_t src_adler32 = fpng_adler32(pSrc, src_len, FPNG_ADLER32_INIT);
+
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			if (dst_ofs + 1 > dst_buf_size)
+				return 0;
+
+			pDst[dst_ofs] = (uint8_t)(src_adler32 >> 24);
+			dst_ofs++;
+
+			src_adler32 <<= 8;
+		}
+
+		return dst_ofs;
+	}
+
+	static void adjust_freq32(uint32_t num_freq, uint32_t* pFreq, uint16_t* pFreq16)
+	{
+		uint32_t total_freq = 0;
+		for (uint32_t i = 0; i < num_freq; i++)
+			total_freq += pFreq[i];
+
+		if (!total_freq)
+		{
+			memset(pFreq16, 0, num_freq * sizeof(uint16_t));
+			return;
+		}
+
+		uint32_t total_freq16 = 0;
+		for (uint32_t i = 0; i < num_freq; i++)
+		{
+			uint64_t f = pFreq[i];
+			if (!f)
+			{
+				pFreq16[i] = 0;
+				continue;
+			}
+
+			pFreq16[i] = (uint16_t)maximum<uint32_t>(1, (uint32_t)((f * UINT16_MAX) / total_freq));
+
+			total_freq16 += pFreq16[i];
+		}
+
+		while (total_freq16 > UINT16_MAX)
+		{
+			total_freq16 = 0;
+			for (uint32_t i = 0; i < num_freq; i++)
+			{
+				if (pFreq[i])
+				{
+					pFreq[i] = maximum<uint32_t>(1, pFreq[i] >> 1);
+					total_freq16 += pFreq[i];
+				}
+			}
+		}
+	}
+
+#if FPNG_TRAIN_HUFFMAN_TABLES
+	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)
+	{
+		assert((num_chans == 3) || (num_chans == 4));
+		assert(HUFF_COUNTS_SIZE == DEFL_MAX_HUFF_SYMBOLS_0); // must be equal
+				
+		defl_huff dh;
+		memset(&dh, 0, sizeof(dh));
+
+		uint32_t lit_freq[DEFL_MAX_HUFF_SYMBOLS_0];
+		
+		uint32_t shift_len = 0;
+		for (; ; )
+		{
+			uint32_t i;
+			for (i = 0; i < DEFL_MAX_HUFF_SYMBOLS_0; i++)
+			{
+				uint64_t f = pFreq[i];
+				if (f)
+					f = maximum<uint64_t>(1U, f >> shift_len);
+
+				if (f > UINT32_MAX)
+					break;
+
+				lit_freq[i] = (uint32_t)pFreq[i];
+			}
+
+			if (i == DEFL_MAX_HUFF_SYMBOLS_0)
+				break;
+			
+			shift_len++;
+		}
+				
+		// Ensure all valid Deflate literal/EOB/length syms are non-zero, so anything can be coded.
+		for (uint32_t i = 0; i <= 256; i++)
+		{
+			if (!lit_freq[i])
+				lit_freq[i] = 1;
+		}
+
+		for (uint32_t len = num_chans; len <= DEFL_MAX_MATCH_LEN; len += num_chans)
+		{
+			uint32_t sym = g_defl_len_sym[len - 3];
+			if (!lit_freq[sym])
+				lit_freq[sym] = 1;
+		}
+
+		adjust_freq32(DEFL_MAX_HUFF_SYMBOLS_0, lit_freq, &dh.m_huff_count[0][0]);
+		
+		const uint32_t dist_sym = g_defl_small_dist_sym[num_chans - 1];
+		dh.m_huff_count[1][dist_sym] = 1;
+		dh.m_huff_count[1][dist_sym + 1] = 1; // to workaround a bug in wuffs decoder
+			
+		prefix.resize(4096);
+		uint8_t* pDst = prefix.data();
+		uint32_t dst_buf_size = (uint32_t)prefix.size();
+
+		uint32_t dst_ofs = 0;
+
+		// zlib header
+		PUT_BITS(0x78, 8);
+		PUT_BITS(0x01, 8);
+
+		// write BFINAL bit
+		PUT_BITS(1, 1);
+				
+		if (!defl_start_dynamic_block(&dh, pDst, dst_ofs, dst_buf_size, bit_buf, bit_buf_size))
+			return false;
+
+		prefix.resize(dst_ofs);
+
+		for (uint32_t i = 0; i < DEFL_MAX_HUFF_SYMBOLS_0; i++)
+		{
+			pCodes[i] = dh.m_huff_codes[0][i];
+			pCodesizes[i] = dh.m_huff_code_sizes[0][i];
+		}
+
+		return true;
+	}
+#endif
+
+	static uint32_t pixel_deflate_dyn_3_rle(
+		const uint8_t* pImg, uint32_t w, uint32_t h,
+		uint8_t* pDst, uint32_t dst_buf_size)
+	{
+		const uint32_t bpl = 1 + w * 3;
+
+		uint64_t bit_buf = 0;
+		int bit_buf_size = 0;
+
+		uint32_t dst_ofs = 0;
+
+		// zlib header
+		PUT_BITS(0x78, 8);
+		PUT_BITS(0x01, 8);
+
+		// write BFINAL bit
+		PUT_BITS(1, 1);
+
+		std::vector<uint32_t> codes((w + 1) * h);
+		uint32_t* pDst_codes = codes.data();
+
+		uint32_t lit_freq[DEFL_MAX_HUFF_SYMBOLS_0];
+		memset(lit_freq, 0, sizeof(lit_freq));
+		
+		const uint8_t* pSrc = pImg;
+		uint32_t src_ofs = 0;
+
+		uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT);
+
+		const uint32_t dist_sym = g_defl_small_dist_sym[3 - 1];
+				
+		for (uint32_t y = 0; y < h; y++)
+		{
+			const uint32_t end_src_ofs = src_ofs + bpl;
+
+			const uint32_t filter_lit = pSrc[src_ofs++];
+			*pDst_codes++ = 1 | (filter_lit << 8);
+			lit_freq[filter_lit]++;
+
+			uint32_t prev_lits;
+
+			{
+				uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs);
+
+				*pDst_codes++ = lits << 8;
+
+				lit_freq[lits & 0xFF]++;
+				lit_freq[(lits >> 8) & 0xFF]++;
+				lit_freq[lits >> 16]++;
+
+				src_ofs += 3;
+
+				prev_lits = lits;
+			}
+
+			while (src_ofs < end_src_ofs)
+			{
+				uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs);
+
+				if (lits == prev_lits)
+				{
+					uint32_t match_len = 3;
+					uint32_t max_match_len = minimum<int>(255, (int)(end_src_ofs - src_ofs));
+
+					while (match_len < max_match_len)
+					{
+						if (READ_RGB_PIXEL(pSrc + src_ofs + match_len) != lits)
+							break;
+						match_len += 3;
+					}
+										
+					*pDst_codes++ = match_len - 1;
+
+					uint32_t adj_match_len = match_len - 3;
+
+					lit_freq[g_defl_len_sym[adj_match_len]]++;
+					
+					src_ofs += match_len;
+				}
+				else
+				{
+					*pDst_codes++ = lits << 8;
+
+					lit_freq[lits & 0xFF]++;
+					lit_freq[(lits >> 8) & 0xFF]++;
+					lit_freq[lits >> 16]++;
+
+					prev_lits = lits;
+
+					src_ofs += 3;
+				}
+
+			} // while (src_ofs < end_src_ofs)
+
+		} // y
+
+		assert(src_ofs == h * bpl);
+		const uint32_t total_codes = (uint32_t)(pDst_codes - codes.data());
+		assert(total_codes <= codes.size());
+								
+		defl_huff dh;
+		
+		lit_freq[256] = 1;
+
+		adjust_freq32(DEFL_MAX_HUFF_SYMBOLS_0, lit_freq, &dh.m_huff_count[0][0]);
+
+		memset(&dh.m_huff_count[1][0], 0, sizeof(dh.m_huff_count[1][0]) * DEFL_MAX_HUFF_SYMBOLS_1);
+		dh.m_huff_count[1][dist_sym] = 1;
+		dh.m_huff_count[1][dist_sym + 1] = 1; // to workaround a bug in wuffs decoder
+
+		if (!defl_start_dynamic_block(&dh, pDst, dst_ofs, dst_buf_size, bit_buf, bit_buf_size))
+			return 0;
+
+		assert(bit_buf_size <= 7);
+		assert(dh.m_huff_codes[1][dist_sym] == 0 && dh.m_huff_code_sizes[1][dist_sym] == 1);
+				
+		for (uint32_t i = 0; i < total_codes; i++)
+		{
+			uint32_t c = codes[i];
+
+			uint32_t c_type = c & 0xFF;
+			if (c_type == 0)
+			{
+				uint32_t lits = c >> 8;
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]);
+				lits >>= 8;
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]);
+				lits >>= 8;
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits], dh.m_huff_code_sizes[0][lits]);
+			}
+			else if (c_type == 1)
+			{
+				uint32_t lit = c >> 8;
+				PUT_BITS_CZ(dh.m_huff_codes[0][lit], dh.m_huff_code_sizes[0][lit]);
+			}
+			else
+			{
+				uint32_t match_len = c_type + 1;
+
+				uint32_t adj_match_len = match_len - 3;
+				
+				PUT_BITS_CZ(dh.m_huff_codes[0][g_defl_len_sym[adj_match_len]], dh.m_huff_code_sizes[0][g_defl_len_sym[adj_match_len]]);
+				PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], g_defl_len_extra[adj_match_len] + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0
+
+				// no need to write the distance code, it's always 0
+				//PUT_BITS_CZ(dh.m_huff_codes[1][dist_sym], dh.m_huff_code_sizes[1][dist_sym]);
+			}
+
+			// up to 55 bits
+			PUT_BITS_FLUSH;
+		}
+
+		PUT_BITS_CZ(dh.m_huff_codes[0][256], dh.m_huff_code_sizes[0][256]);
+
+		PUT_BITS_FORCE_FLUSH;
+
+		// Write zlib adler32
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			if ((dst_ofs + 1) > dst_buf_size)
+				return 0;
+			*(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24);
+			dst_ofs++;
+
+			src_adler32 <<= 8;
+		}
+
+		return dst_ofs;
+	}
+
+	static uint32_t pixel_deflate_dyn_3_rle_one_pass(
+		const uint8_t* pImg, uint32_t w, uint32_t h,
+		uint8_t* pDst, uint32_t dst_buf_size)
+	{
+		const uint32_t bpl = 1 + w * 3;
+
+		if (dst_buf_size < sizeof(g_dyn_huff_3))
+			return false;
+		memcpy(pDst, g_dyn_huff_3, sizeof(g_dyn_huff_3));
+		uint32_t dst_ofs = sizeof(g_dyn_huff_3);
+
+		uint64_t bit_buf = DYN_HUFF_3_BITBUF;
+		int bit_buf_size = DYN_HUFF_3_BITBUF_SIZE;
+
+		const uint8_t* pSrc = pImg;
+		uint32_t src_ofs = 0;
+
+		uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT);
+
+		for (uint32_t y = 0; y < h; y++)
+		{
+			const uint32_t end_src_ofs = src_ofs + bpl;
+
+			const uint32_t filter_lit = pSrc[src_ofs++];
+			PUT_BITS_CZ(g_dyn_huff_3_codes[filter_lit].m_code, g_dyn_huff_3_codes[filter_lit].m_code_size);
+
+			uint32_t prev_lits;
+
+			{
+				uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs);
+
+				PUT_BITS_CZ(g_dyn_huff_3_codes[lits & 0xFF].m_code, g_dyn_huff_3_codes[lits & 0xFF].m_code_size);
+				PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code_size);
+				PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 16)].m_code, g_dyn_huff_3_codes[(lits >> 16)].m_code_size);
+
+				src_ofs += 3;
+			
+				prev_lits = lits;
+			}
+
+			PUT_BITS_FLUSH;
+
+			while (src_ofs < end_src_ofs)
+			{
+				uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs);
+
+				if (lits == prev_lits)
+				{
+					uint32_t match_len = 3;
+					uint32_t max_match_len = minimum<int>(255, (int)(end_src_ofs - src_ofs));
+
+					while (match_len < max_match_len)
+					{
+						if (READ_RGB_PIXEL(pSrc + src_ofs + match_len) != lits)
+							break;
+						match_len += 3;
+					}
+										
+					uint32_t adj_match_len = match_len - 3;
+
+					PUT_BITS_CZ(g_dyn_huff_3_codes[g_defl_len_sym[adj_match_len]].m_code, g_dyn_huff_3_codes[g_defl_len_sym[adj_match_len]].m_code_size);
+					PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], g_defl_len_extra[adj_match_len] + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0
+
+					src_ofs += match_len;
+				}
+				else
+				{
+					PUT_BITS_CZ(g_dyn_huff_3_codes[lits & 0xFF].m_code, g_dyn_huff_3_codes[lits & 0xFF].m_code_size);
+					PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code_size);
+					PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 16)].m_code, g_dyn_huff_3_codes[(lits >> 16)].m_code_size);
+					
+					prev_lits = lits;
+
+					src_ofs += 3;
+				}
+
+				PUT_BITS_FLUSH;
+
+			} // while (src_ofs < end_src_ofs)
+
+		} // y
+
+		assert(src_ofs == h * bpl);
+		
+		assert(bit_buf_size <= 7);
+
+		PUT_BITS_CZ(g_dyn_huff_3_codes[256].m_code, g_dyn_huff_3_codes[256].m_code_size);
+
+		PUT_BITS_FORCE_FLUSH;
+
+		// Write zlib adler32
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			if ((dst_ofs + 1) > dst_buf_size)
+				return 0;
+			*(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24);
+			dst_ofs++;
+
+			src_adler32 <<= 8;
+		}
+
+		return dst_ofs;
+	}
+
+	static uint32_t pixel_deflate_dyn_4_rle(
+		const uint8_t* pImg, uint32_t w, uint32_t h,
+		uint8_t* pDst, uint32_t dst_buf_size)
+	{
+		const uint32_t bpl = 1 + w * 4;
+
+		uint64_t bit_buf = 0;
+		int bit_buf_size = 0;
+
+		uint32_t dst_ofs = 0;
+
+		// zlib header
+		PUT_BITS(0x78, 8);
+		PUT_BITS(0x01, 8);
+
+		// write BFINAL bit
+		PUT_BITS(1, 1);
+
+		std::vector<uint64_t> codes;
+		codes.resize((w + 1) * h);
+		uint64_t* pDst_codes = codes.data();
+
+		uint32_t lit_freq[DEFL_MAX_HUFF_SYMBOLS_0];
+		memset(lit_freq, 0, sizeof(lit_freq));
+
+		const uint8_t* pSrc = pImg;
+		uint32_t src_ofs = 0;
+
+		uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT);
+
+		const uint32_t dist_sym = g_defl_small_dist_sym[4 - 1];
+
+		for (uint32_t y = 0; y < h; y++)
+		{
+			const uint32_t end_src_ofs = src_ofs + bpl;
+
+			const uint32_t filter_lit = pSrc[src_ofs++];
+			*pDst_codes++ = 1 | (filter_lit << 8);
+			lit_freq[filter_lit]++;
+
+			uint32_t prev_lits;
+			{
+				uint32_t lits = READ_LE32(pSrc + src_ofs);
+
+				*pDst_codes++ = (uint64_t)lits << 8;
+
+				lit_freq[lits & 0xFF]++;
+				lit_freq[(lits >> 8) & 0xFF]++;
+				lit_freq[(lits >> 16) & 0xFF]++;
+				lit_freq[lits >> 24]++;
+
+				src_ofs += 4;
+				
+				prev_lits = lits;
+			}
+
+			while (src_ofs < end_src_ofs)
+			{
+				uint32_t lits = READ_LE32(pSrc + src_ofs);
+
+				if (lits == prev_lits)
+				{
+					uint32_t match_len = 4;
+					uint32_t max_match_len = minimum<int>(252, (int)(end_src_ofs - src_ofs));
+
+					while (match_len < max_match_len)
+					{
+						if (READ_LE32(pSrc + src_ofs + match_len) != lits)
+							break;
+						match_len += 4;
+					}
+										
+					*pDst_codes++ = match_len - 1;
+
+					uint32_t adj_match_len = match_len - 3;
+
+					lit_freq[g_defl_len_sym[adj_match_len]]++;
+					
+					src_ofs += match_len;
+				}
+				else
+				{
+					*pDst_codes++ = (uint64_t)lits << 8;
+
+					lit_freq[lits & 0xFF]++;
+					lit_freq[(lits >> 8) & 0xFF]++;
+					lit_freq[(lits >> 16) & 0xFF]++;
+					lit_freq[lits >> 24]++;
+					
+					prev_lits = lits;
+
+					src_ofs += 4;
+				}
+
+			} // while (src_ofs < end_src_ofs)
+
+		} // y
+
+		assert(src_ofs == h * bpl);
+		const uint32_t total_codes = (uint32_t)(pDst_codes - codes.data());
+		assert(total_codes <= codes.size());
+						
+		defl_huff dh;
+		
+		lit_freq[256] = 1;
+
+		adjust_freq32(DEFL_MAX_HUFF_SYMBOLS_0, lit_freq, &dh.m_huff_count[0][0]);
+		
+		memset(&dh.m_huff_count[1][0], 0, sizeof(dh.m_huff_count[1][0]) * DEFL_MAX_HUFF_SYMBOLS_1);
+		dh.m_huff_count[1][dist_sym] = 1;
+		dh.m_huff_count[1][dist_sym + 1] = 1; // to workaround a bug in wuffs decoder
+
+		if (!defl_start_dynamic_block(&dh, pDst, dst_ofs, dst_buf_size, bit_buf, bit_buf_size))
+			return 0;
+
+		assert(bit_buf_size <= 7);
+		assert(dh.m_huff_codes[1][dist_sym] == 0 && dh.m_huff_code_sizes[1][dist_sym] == 1);
+
+		for (uint32_t i = 0; i < total_codes; i++)
+		{
+			uint64_t c = codes[i];
+
+			uint32_t c_type = (uint32_t)(c & 0xFF);
+			if (c_type == 0)
+			{
+				uint32_t lits = (uint32_t)(c >> 8);
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]);
+				lits >>= 8;
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]);
+				lits >>= 8;
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]);
+				lits >>= 8;
+
+				if (bit_buf_size >= 49)
+				{
+					PUT_BITS_FLUSH;
+				}
+
+				PUT_BITS_CZ(dh.m_huff_codes[0][lits], dh.m_huff_code_sizes[0][lits]);
+			}
+			else if (c_type == 1)
+			{
+				uint32_t lit = (uint32_t)(c >> 8);
+				PUT_BITS_CZ(dh.m_huff_codes[0][lit], dh.m_huff_code_sizes[0][lit]);
+			}
+			else
+			{
+				uint32_t match_len = c_type + 1;
+
+				uint32_t adj_match_len = match_len - 3;
+				
+				PUT_BITS_CZ(dh.m_huff_codes[0][g_defl_len_sym[adj_match_len]], dh.m_huff_code_sizes[0][g_defl_len_sym[adj_match_len]]);
+				PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], g_defl_len_extra[adj_match_len] + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0
+
+				// no need to write the distance code, it's always 0
+			}
+
+			// up to 55 bits
+			PUT_BITS_FLUSH;
+		}
+
+		PUT_BITS_CZ(dh.m_huff_codes[0][256], dh.m_huff_code_sizes[0][256]);
+
+		PUT_BITS_FORCE_FLUSH;
+
+		// Write zlib adler32
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			if ((dst_ofs + 1) > dst_buf_size)
+				return 0;
+			*(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24);
+			dst_ofs++;
+
+			src_adler32 <<= 8;
+		}
+
+		return dst_ofs;
+	}
+
+	static uint32_t pixel_deflate_dyn_4_rle_one_pass(
+		const uint8_t* pImg, uint32_t w, uint32_t h,
+		uint8_t* pDst, uint32_t dst_buf_size)
+	{
+		const uint32_t bpl = 1 + w * 4;
+
+		if (dst_buf_size < sizeof(g_dyn_huff_4))
+			return false;
+		memcpy(pDst, g_dyn_huff_4, sizeof(g_dyn_huff_4));
+		uint32_t dst_ofs = sizeof(g_dyn_huff_4);
+
+		uint64_t bit_buf = DYN_HUFF_4_BITBUF;
+		int bit_buf_size = DYN_HUFF_4_BITBUF_SIZE;
+
+		const uint8_t* pSrc = pImg;
+		uint32_t src_ofs = 0;
+
+		uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT);
+
+		for (uint32_t y = 0; y < h; y++)
+		{
+			const uint32_t end_src_ofs = src_ofs + bpl;
+
+			const uint32_t filter_lit = pSrc[src_ofs++];
+			PUT_BITS_CZ(g_dyn_huff_4_codes[filter_lit].m_code, g_dyn_huff_4_codes[filter_lit].m_code_size);
+
+			PUT_BITS_FLUSH;
+
+			uint32_t prev_lits;
+			{
+				uint32_t lits = READ_LE32(pSrc + src_ofs);
+
+				PUT_BITS_CZ(g_dyn_huff_4_codes[lits & 0xFF].m_code, g_dyn_huff_4_codes[lits & 0xFF].m_code_size);
+				PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code_size);
+				PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code_size);
+
+				if (bit_buf_size >= 49)
+				{
+					PUT_BITS_FLUSH;
+				}
+				
+				PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 24)].m_code, g_dyn_huff_4_codes[(lits >> 24)].m_code_size);
+
+				src_ofs += 4;
+				
+				prev_lits = lits;
+			}
+
+			PUT_BITS_FLUSH;
+
+			while (src_ofs < end_src_ofs)
+			{
+				uint32_t lits = READ_LE32(pSrc + src_ofs);
+								
+				if (lits == prev_lits)
+				{
+					uint32_t match_len = 4;
+					uint32_t max_match_len = minimum<int>(252, (int)(end_src_ofs - src_ofs));
+
+					while (match_len < max_match_len)
+					{
+						if (READ_LE32(pSrc + src_ofs + match_len) != lits)
+							break;
+						match_len += 4;
+					}
+
+					uint32_t adj_match_len = match_len - 3;
+
+					const uint32_t match_code_bits = g_dyn_huff_4_codes[g_defl_len_sym[adj_match_len]].m_code_size;
+					const uint32_t len_extra_bits = g_defl_len_extra[adj_match_len];
+
+					if (match_len == 4)
+					{
+						// This check is optional - see if just encoding 4 literals would be cheaper than using a short match.
+						uint32_t lit_bits = g_dyn_huff_4_codes[lits & 0xFF].m_code_size + g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code_size + 
+							g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code_size + g_dyn_huff_4_codes[(lits >> 24)].m_code_size;
+						
+						if ((match_code_bits + len_extra_bits + 1) > lit_bits)
+							goto do_literals;
+					}
+
+					PUT_BITS_CZ(g_dyn_huff_4_codes[g_defl_len_sym[adj_match_len]].m_code, match_code_bits);
+					PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], len_extra_bits + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0
+
+					src_ofs += match_len;
+				}
+				else
+				{
+do_literals:
+					PUT_BITS_CZ(g_dyn_huff_4_codes[lits & 0xFF].m_code, g_dyn_huff_4_codes[lits & 0xFF].m_code_size);
+					PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code_size);
+					PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code_size);
+
+					if (bit_buf_size >= 49)
+					{
+						PUT_BITS_FLUSH;
+					}
+
+					PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 24)].m_code, g_dyn_huff_4_codes[(lits >> 24)].m_code_size);
+
+					src_ofs += 4;
+					
+					prev_lits = lits;
+				}
+
+				PUT_BITS_FLUSH;
+
+			} // while (src_ofs < end_src_ofs)
+
+		} // y
+
+		assert(src_ofs == h * bpl);
+
+		assert(bit_buf_size <= 7);
+
+		PUT_BITS_CZ(g_dyn_huff_4_codes[256].m_code, g_dyn_huff_4_codes[256].m_code_size);
+
+		PUT_BITS_FORCE_FLUSH;
+
+		// Write zlib adler32
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			if ((dst_ofs + 1) > dst_buf_size)
+				return 0;
+			*(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24);
+			dst_ofs++;
+
+			src_adler32 <<= 8;
+		}
+
+		return dst_ofs;
+	}
+
+	static void vector_append(std::vector<uint8_t>& buf, const void* pData, size_t len)
+	{
+		if (len)
+		{
+			size_t l = buf.size();
+			buf.resize(l + len);
+			memcpy(buf.data() + l, pData, len);
+		}
+	}
+		
+	static void apply_filter(uint32_t filter, int w, int h, uint32_t num_chans, uint32_t bpl, const uint8_t* pSrc, const uint8_t* pPrev_src, uint8_t* pDst)
+	{
+		(void)h;
+
+		switch (filter)
+		{
+		case 0:
+		{
+			*pDst++ = 0;
+
+			memcpy(pDst, pSrc, bpl);
+			break;
+		}
+		case 2:
+		{
+			assert(pPrev_src);
+
+			// Previous scanline
+			*pDst++ = 2;
+
+#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE
+			if (g_cpu_info.can_use_sse41())
+			{
+				uint32_t bytes_to_process = w * num_chans, ofs = 0;
+				for (; bytes_to_process >= 16; bytes_to_process -= 16, ofs += 16)
+					_mm_storeu_si128((__m128i*)(pDst + ofs), _mm_sub_epi8(_mm_loadu_si128((const __m128i*)(pSrc + ofs)), _mm_loadu_si128((const __m128i*)(pPrev_src + ofs))));
+
+				for (; bytes_to_process; bytes_to_process--, ofs++)
+					pDst[ofs] = (uint8_t)(pSrc[ofs] - pPrev_src[ofs]);
+			}
+			else
+#endif
+			{
+				if (num_chans == 3)
+				{
+					for (uint32_t x = 0; x < (uint32_t)w; x++)
+					{
+						pDst[0] = (uint8_t)(pSrc[0] - pPrev_src[0]);
+						pDst[1] = (uint8_t)(pSrc[1] - pPrev_src[1]);
+						pDst[2] = (uint8_t)(pSrc[2] - pPrev_src[2]);
+
+						pSrc += 3;
+						pPrev_src += 3;
+						pDst += 3;
+					}
+				}
+				else
+				{
+					for (uint32_t x = 0; x < (uint32_t)w; x++)
+					{
+						pDst[0] = (uint8_t)(pSrc[0] - pPrev_src[0]);
+						pDst[1] = (uint8_t)(pSrc[1] - pPrev_src[1]);
+						pDst[2] = (uint8_t)(pSrc[2] - pPrev_src[2]);
+						pDst[3] = (uint8_t)(pSrc[3] - pPrev_src[3]);
+
+						pSrc += 4;
+						pPrev_src += 4;
+						pDst += 4;
+					}
+				}
+			}
+
+			break;
+		}
+		default:
+			assert(0);
+			break;
+		}
+	}
+
+	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)
+	{
+		if (!endian_check())
+		{
+			assert(0);
+			return false;
+		}
+
+		if ((w < 1) || (h < 1) || (w * (uint64_t)h > UINT32_MAX) || (w > FPNG_MAX_SUPPORTED_DIM) || (h > FPNG_MAX_SUPPORTED_DIM))
+		{
+			assert(0);
+			return false;
+		}
+
+		if ((num_chans != 3) && (num_chans != 4))
+		{
+			assert(0);
+			return false;
+		}
+
+		int i, bpl = w * num_chans;
+		uint32_t y;
+
+		std::vector<uint8_t> temp_buf;
+		temp_buf.resize((bpl + 1) * h + 7);
+		uint32_t temp_buf_ofs = 0;
+
+		for (y = 0; y < h; ++y)
+		{
+			const uint8_t* pSrc = (uint8_t*)pImage + y * bpl;
+			const uint8_t* pPrev_src = y ? ((uint8_t*)pImage + (y - 1) * bpl) : nullptr;
+
+			uint8_t* pDst = &temp_buf[temp_buf_ofs];
+
+			apply_filter(y ? 2 : 0, w, h, num_chans, bpl, pSrc, pPrev_src, pDst);
+
+			temp_buf_ofs += 1 + bpl;
+		}
+
+		const uint32_t PNG_HEADER_SIZE = 58;
+				
+		uint32_t out_ofs = PNG_HEADER_SIZE;
+				
+		out_buf.resize((out_ofs + (bpl + 1) * h + 7) & ~7);
+
+		uint32_t defl_size = 0;
+		if ((flags & FPNG_FORCE_UNCOMPRESSED) == 0)
+		{
+			if (num_chans == 3)
+			{
+				if (flags & FPNG_ENCODE_SLOWER)
+					defl_size = pixel_deflate_dyn_3_rle(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs);
+				else
+					defl_size = pixel_deflate_dyn_3_rle_one_pass(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs);
+			}
+			else
+			{
+				if (flags & FPNG_ENCODE_SLOWER)
+					defl_size = pixel_deflate_dyn_4_rle(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs);
+				else
+					defl_size = pixel_deflate_dyn_4_rle_one_pass(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs);
+			}
+		}
+
+		uint32_t zlib_size = defl_size;
+		
+		if (!defl_size)
+		{
+			// Dynamic block failed to compress - fall back to uncompressed blocks, filter 0.
+
+			temp_buf_ofs = 0;
+
+			for (y = 0; y < h; ++y)
+			{
+				const uint8_t* pSrc = (uint8_t*)pImage + y * bpl;
+
+				uint8_t* pDst = &temp_buf[temp_buf_ofs];
+
+				apply_filter(0, w, h, num_chans, bpl, pSrc, nullptr, pDst);
+
+				temp_buf_ofs += 1 + bpl;
+			}
+
+			assert(temp_buf_ofs <= temp_buf.size());
+						
+			out_buf.resize(out_ofs + 6 + temp_buf_ofs + ((temp_buf_ofs + 65534) / 65535) * 5);
+
+			uint32_t raw_size = write_raw_block(temp_buf.data(), (uint32_t)temp_buf_ofs, out_buf.data() + out_ofs, (uint32_t)out_buf.size() - out_ofs);
+			if (!raw_size)
+			{
+				// Somehow we miscomputed the size of the output buffer.
+				assert(0);
+				return false;
+			}
+
+			zlib_size = raw_size;
+		}
+		
+		assert((out_ofs + zlib_size) <= out_buf.size());
+
+		out_buf.resize(out_ofs + zlib_size);
+
+		const uint32_t idat_len = (uint32_t)out_buf.size() - PNG_HEADER_SIZE;
+
+		// Write real PNG header, fdEC chunk, and the beginning of the IDAT chunk
+		{
+			static const uint8_t s_color_type[] = { 0x00, 0x00, 0x04, 0x02, 0x06 };
+
+			uint8_t pnghdr[58] = { 
+				0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,   // PNG sig
+				0x00,0x00,0x00,0x0d, 'I','H','D','R',  // IHDR chunk len, type
+			    0,0,(uint8_t)(w >> 8),(uint8_t)w, // width
+				0,0,(uint8_t)(h >> 8),(uint8_t)h, // height
+				8,   //bit_depth
+				s_color_type[num_chans], // color_type
+				0, // compression
+				0, // filter
+				0, // interlace
+				0, 0, 0, 0, // IHDR crc32
+				0, 0, 0, 5, 'f', 'd', 'E', 'C', 82, 36, 147, 227, FPNG_FDEC_VERSION,   0xE5, 0xAB, 0x62, 0x99, // our custom private, ancillary, do not copy, fdEC chunk
+			  (uint8_t)(idat_len >> 24),(uint8_t)(idat_len >> 16),(uint8_t)(idat_len >> 8),(uint8_t)idat_len, 'I','D','A','T' // IDATA chunk len, type
+			}; 
+
+			// Compute IHDR CRC32
+			uint32_t c = (uint32_t)fpng_crc32(pnghdr + 12, 17, FPNG_CRC32_INIT);
+			for (i = 0; i < 4; ++i, c <<= 8)
+				((uint8_t*)(pnghdr + 29))[i] = (uint8_t)(c >> 24);
+
+			memcpy(out_buf.data(), pnghdr, PNG_HEADER_SIZE);
+		}
+
+		// Write IDAT chunk's CRC32 and a 0 length IEND chunk
+		vector_append(out_buf, "\0\0\0\0\0\0\0\0\x49\x45\x4e\x44\xae\x42\x60\x82", 16); // IDAT CRC32, followed by the IEND chunk
+
+		// Compute IDAT crc32
+		uint32_t c = (uint32_t)fpng_crc32(out_buf.data() + PNG_HEADER_SIZE - 4, idat_len + 4, FPNG_CRC32_INIT);
+		
+		for (i = 0; i < 4; ++i, c <<= 8)
+			(out_buf.data() + out_buf.size() - 16)[i] = (uint8_t)(c >> 24);
+				
+		return true;
+	}
+
+#ifndef FPNG_NO_STDIO
+	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)
+	{
+		std::vector<uint8_t> out_buf;
+		if (!fpng_encode_image_to_memory(pImage, w, h, num_chans, out_buf, flags))
+			return false;
+
+		FILE* pFile = nullptr;
+#ifdef _MSC_VER
+		fopen_s(&pFile, pFilename, "wb");
+#else
+		pFile = fopen(pFilename, "wb");
+#endif
+		if (!pFile)
+			return false;
+
+		if (fwrite(out_buf.data(), 1, out_buf.size(), pFile) != out_buf.size())
+		{
+			fclose(pFile);
+			return false;
+		}
+
+		return (fclose(pFile) != EOF);
+	}
+#endif
+
+	// Decompression
+
+	const uint32_t FPNG_DECODER_TABLE_BITS = 12;
+	const uint32_t FPNG_DECODER_TABLE_SIZE = 1 << FPNG_DECODER_TABLE_BITS;
+
+	static bool build_decoder_table(uint32_t num_syms, uint8_t* pCode_sizes, uint32_t* pTable)
+	{
+		uint32_t num_codes[16];
+
+		memset(num_codes, 0, sizeof(num_codes));
+		for (uint32_t i = 0; i < num_syms; i++)
+		{
+			assert(pCode_sizes[i] <= FPNG_DECODER_TABLE_SIZE);
+			num_codes[pCode_sizes[i]]++;
+		}
+
+		uint32_t next_code[17];
+		next_code[0] = next_code[1] = 0;
+		uint32_t total = 0;
+		for (uint32_t i = 1; i <= 15; i++)
+			next_code[i + 1] = (uint32_t)(total = ((total + ((uint32_t)num_codes[i])) << 1));
+
+		if (total != 0x10000)
+		{
+			uint32_t j = 0;
+
+			for (uint32_t i = 15; i != 0; i--)
+				if ((j += num_codes[i]) > 1)
+					return false;
+			
+			if (j != 1)
+				return false;
+		}
+
+		uint32_t rev_codes[DEFL_MAX_HUFF_SYMBOLS];
+
+		for (uint32_t i = 0; i < num_syms; i++)
+			rev_codes[i] = next_code[pCode_sizes[i]]++;
+
+		memset(pTable, 0, sizeof(uint32_t) * FPNG_DECODER_TABLE_SIZE);
+
+		for (uint32_t i = 0; i < num_syms; i++)
+		{
+			const uint32_t code_size = pCode_sizes[i];
+			if (!code_size)
+				continue;
+
+			uint32_t old_code = rev_codes[i], new_code = 0;
+			for (uint32_t j = code_size; j != 0; j--)
+			{
+				new_code = (new_code << 1) | (old_code & 1);
+				old_code >>= 1;
+			}
+
+			uint32_t j = 1 << code_size;
+
+			while (new_code < FPNG_DECODER_TABLE_SIZE)
+			{
+				pTable[new_code] = i | (code_size << 9);
+				new_code += j;
+			}
+		}
+
+		return true;
+	}
+
+	static const uint16_t g_run_len3_to_4[259] = 
+	{
+		0,
+		0, 0, 4, 0, 0, 8, 0, 0, 12, 0, 0, 16, 0, 0, 20, 0, 0, 24, 0, 0, 28, 0, 0,
+		32, 0, 0, 36, 0, 0, 40, 0, 0, 44, 0, 0, 48, 0, 0, 52, 0, 0, 56, 0, 0,
+		60, 0, 0, 64, 0, 0, 68, 0, 0, 72, 0, 0, 76, 0, 0, 80, 0, 0, 84, 0, 0,
+		88, 0, 0, 92, 0, 0, 96, 0, 0, 100, 0, 0, 104, 0, 0, 108, 0, 0, 112, 0, 0,
+		116, 0, 0, 120, 0, 0, 124, 0, 0, 128, 0, 0, 132, 0, 0, 136, 0, 0, 140, 0, 0,
+		144, 0, 0, 148, 0, 0, 152, 0, 0, 156, 0, 0, 160, 0, 0, 164, 0, 0, 168, 0, 0,
+		172, 0, 0, 176, 0, 0, 180, 0, 0, 184, 0, 0, 188, 0, 0, 192, 0, 0, 196, 0, 0,
+		200, 0, 0, 204, 0, 0, 208, 0, 0, 212, 0, 0, 216, 0, 0, 220, 0, 0, 224, 0, 0,
+		228, 0, 0, 232, 0, 0, 236, 0, 0, 240, 0, 0, 244, 0, 0, 248, 0, 0, 252, 0, 0,
+		256, 0, 0, 260, 0, 0, 264, 0, 0, 268, 0, 0, 272, 0, 0, 276, 0, 0, 280, 0, 0,
+		284, 0, 0, 288, 0, 0, 292, 0, 0, 296, 0, 0, 300, 0, 0, 304, 0, 0, 308, 0, 0,
+		312, 0, 0, 316, 0, 0, 320, 0, 0, 324, 0, 0, 328, 0, 0, 332, 0, 0, 336, 0, 0,
+		340, 0, 0, 
+		344,
+	};
+
+	static const int s_length_extra[] = { 0,0,0,0, 0,0,0,0, 1,1,1,1, 2,2,2,2, 3,3,3,3, 4,4,4,4, 5,5,5,5, 0,    0,0 };
+	static const int s_length_range[] = { 3,4,5,6, 7,8,9,10, 11,13,15,17, 19,23,27,31, 35,43,51,59, 67,83,99,115, 131,163,195,227, 258,    0,0 };
+
+#define ENSURE_32BITS() do { \
+	if (bit_buf_size < 32) { \
+		if ((src_ofs + 4) > src_len) return false; \
+		bit_buf |= ((uint64_t)READ_LE32(pSrc + src_ofs)) << bit_buf_size; \
+		src_ofs += 4; bit_buf_size += 32; } \
+	} while(0)
+
+#define GET_BITS(b, ll) do { \
+	uint32_t l = ll; assert(l && (l <= 32)); \
+	b = (uint32_t)(bit_buf & g_bitmasks[l]); \
+	bit_buf >>= l; \
+	bit_buf_size -= l; \
+	ENSURE_32BITS(); \
+	} while(0)
+
+#define SKIP_BITS(ll) do { \
+	uint32_t l = ll; assert(l <= 32); \
+	bit_buf >>= l; \
+	bit_buf_size -= l; \
+	ENSURE_32BITS(); \
+	} while(0)
+
+#define GET_BITS_NE(b, ll) do { \
+	uint32_t l = ll; assert(l && (l <= 32) && (bit_buf_size >= l)); \
+	b = (uint32_t)(bit_buf & g_bitmasks[l]); \
+	bit_buf >>= l; \
+	bit_buf_size -= l; \
+	} while(0)
+
+#define SKIP_BITS_NE(ll) do { \
+	uint32_t l = ll; assert(l <= 32 && (bit_buf_size >= l)); \
+	bit_buf >>= l; \
+	bit_buf_size -= l; \
+	} while(0)
+
+	static bool prepare_dynamic_block(
+		const uint8_t* pSrc, uint32_t src_len, uint32_t& src_ofs,
+		uint32_t& bit_buf_size, uint64_t& bit_buf,
+		uint32_t* pLit_table, uint32_t num_chans)
+	{
+		static const uint8_t s_bit_length_order[] = { 16, 17, 18, 0, 8,  7,  9, 6, 10,  5, 11, 4, 12,  3, 13, 2, 14,  1, 15 };
+
+		uint32_t num_lit_codes, num_dist_codes, num_clen_codes;
+
+		GET_BITS(num_lit_codes, 5);
+		num_lit_codes += 257;
+
+		GET_BITS(num_dist_codes, 5);
+		num_dist_codes += 1;
+		
+		uint32_t total_codes = num_lit_codes + num_dist_codes;
+		if (total_codes > (DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1))
+			return false;
+
+		uint8_t code_sizes[DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1];
+		memset(code_sizes, 0, sizeof(code_sizes));
+
+		GET_BITS(num_clen_codes, 4);
+		num_clen_codes += 4;
+
+		uint8_t clen_codesizes[DEFL_MAX_HUFF_SYMBOLS_2];
+		memset(clen_codesizes, 0, sizeof(clen_codesizes));
+
+		for (uint32_t i = 0; i < num_clen_codes; i++)
+		{
+			uint32_t len = 0;
+			GET_BITS(len, 3);
+			clen_codesizes[s_bit_length_order[i]] = (uint8_t)len;
+		}
+
+		uint32_t clen_table[FPNG_DECODER_TABLE_SIZE];
+		if (!build_decoder_table(DEFL_MAX_HUFF_SYMBOLS_2, clen_codesizes, clen_table))
+			return false;
+
+		uint32_t min_code_size = 15;
+
+		for (uint32_t cur_code = 0; cur_code < total_codes; )
+		{
+			uint32_t sym = clen_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+			uint32_t sym_len = sym >> 9;
+			if (!sym_len)
+				return false;
+			SKIP_BITS(sym_len);
+			sym &= 511;
+						
+			if (sym <= 15)
+			{
+				// Can't be a fpng Huffman table
+				if (sym > FPNG_DECODER_TABLE_BITS)
+					return false;
+
+				if (sym)
+					min_code_size = minimum(min_code_size, sym);
+
+				code_sizes[cur_code++] = (uint8_t)sym;
+				continue;
+			}
+
+			uint32_t rep_len = 0, rep_code_size = 0;
+
+			switch (sym)
+			{
+			case 16:
+			{
+				GET_BITS(rep_len, 2);
+				rep_len += 3;
+				if (!cur_code)
+					return false;
+				rep_code_size = code_sizes[cur_code - 1];
+				break;
+			}
+			case 17:
+			{
+				GET_BITS(rep_len, 3);
+				rep_len += 3;
+				rep_code_size = 0;
+				break;
+			}
+			case 18:
+			{
+				GET_BITS(rep_len, 7);
+				rep_len += 11;
+				rep_code_size = 0;
+				break;
+			}
+			}
+
+			if ((cur_code + rep_len) > total_codes)
+				return false;
+
+			for (; rep_len; rep_len--)
+				code_sizes[cur_code++] = (uint8_t)rep_code_size;
+		}
+
+		uint8_t lit_codesizes[DEFL_MAX_HUFF_SYMBOLS_0];
+
+		memcpy(lit_codesizes, code_sizes, num_lit_codes);
+		memset(lit_codesizes + num_lit_codes, 0, DEFL_MAX_HUFF_SYMBOLS_0 - num_lit_codes);
+
+		uint32_t total_valid_distcodes = 0;
+		for (uint32_t i = 0; i < num_dist_codes; i++)
+			total_valid_distcodes += (code_sizes[num_lit_codes + i] == 1);
+		
+		// 1 or 2 because the first version of FPNG only issued 1 valid distance code, but that upset wuffs. So we let 1 or 2 through.
+		if ((total_valid_distcodes < 1) || (total_valid_distcodes > 2))
+			return false;
+
+		if (code_sizes[num_lit_codes + (num_chans - 1)] != 1)
+			return false;
+
+		if (total_valid_distcodes == 2)
+		{
+			// If there are two valid distance codes, make sure the first is 1 bit.
+			if (code_sizes[num_lit_codes + num_chans] != 1)
+				return false;
+		}
+						
+		if (!build_decoder_table(num_lit_codes, lit_codesizes, pLit_table))
+			return false;
+
+		// Add next symbol to decoder table, when it fits
+		for (uint32_t i = 0; i < FPNG_DECODER_TABLE_SIZE; i++)
+		{
+			uint32_t sym = pLit_table[i] & 511;
+			if (sym >= 256)
+				continue;
+
+			uint32_t sym_bits = (pLit_table[i] >> 9) & 15;
+			if (!sym_bits)
+				continue;
+			assert(sym_bits <= FPNG_DECODER_TABLE_BITS);
+
+			uint32_t bits_left = FPNG_DECODER_TABLE_BITS - sym_bits;
+			if (bits_left < min_code_size)
+				continue;
+
+			uint32_t next_bits = i >> sym_bits;
+			uint32_t next_sym = pLit_table[next_bits] & 511;
+			uint32_t next_sym_bits = (pLit_table[next_bits] >> 9) & 15;
+			if ((!next_sym_bits) || (bits_left < next_sym_bits))
+				continue;
+
+			pLit_table[i] |= (next_sym << 16) | (next_sym_bits << (16 + 9));
+		}
+
+		return true;
+	}
+		
+	static bool fpng_pixel_zlib_raw_decompress(
+		const uint8_t* pSrc, uint32_t src_len, uint32_t zlib_len,
+		uint8_t* pDst, uint32_t w, uint32_t h,
+		uint32_t src_chans, uint32_t dst_chans)
+	{
+		assert((src_chans == 3) || (src_chans == 4));
+		assert((dst_chans == 3) || (dst_chans == 4));
+		
+		const uint32_t src_bpl = w * src_chans;
+		const uint32_t dst_bpl = w * dst_chans;
+		const uint32_t dst_len = dst_bpl * h;
+
+		uint32_t src_ofs = 2;
+		uint32_t dst_ofs = 0;
+		uint32_t raster_ofs = 0;
+		uint32_t comp_ofs = 0;
+
+		for (; ; )
+		{
+			if ((src_ofs + 1) > src_len)
+				return false;
+
+			const bool bfinal = (pSrc[src_ofs] & 1) != 0;
+			const uint32_t btype = (pSrc[src_ofs] >> 1) & 3;
+			if (btype != 0)
+				return false;
+
+			src_ofs++;
+
+			if ((src_ofs + 4) > src_len)
+				return false;
+			uint32_t len = pSrc[src_ofs + 0] | (pSrc[src_ofs + 1] << 8);
+			uint32_t nlen = pSrc[src_ofs + 2] | (pSrc[src_ofs + 3] << 8);
+			src_ofs += 4;
+
+			if (len != (~nlen & 0xFFFF))
+				return false;
+
+			if ((src_ofs + len) > src_len)
+				return false;
+
+			// Raw blocks are a relatively uncommon case so this isn't well optimized.
+			// Supports 3->4 and 4->3 byte/pixel conversion.
+			for (uint32_t i = 0; i < len; i++)
+			{
+				uint32_t c = pSrc[src_ofs + i];
+
+				if (!raster_ofs)
+				{
+					// Check filter type
+					if (c != 0)
+						return false;
+					
+					assert(!comp_ofs);
+				}
+				else
+				{
+					if (comp_ofs < dst_chans)
+					{
+						if (dst_ofs == dst_len)
+							return false;
+
+						pDst[dst_ofs++] = (uint8_t)c;
+					}
+					
+					if (++comp_ofs == src_chans)
+					{
+						if (dst_chans > src_chans)
+						{
+							if (dst_ofs == dst_len)
+								return false;
+
+							pDst[dst_ofs++] = (uint8_t)0xFF;
+						}
+
+						comp_ofs = 0;
+					}
+				}
+
+				if (++raster_ofs == (src_bpl + 1))
+				{
+					assert(!comp_ofs);
+					raster_ofs = 0;
+				}
+			}
+
+			src_ofs += len;
+
+			if (bfinal)
+				break;
+		}
+
+		if (comp_ofs != 0)
+			return false;
+
+		// Check for zlib adler32
+		if ((src_ofs + 4) != zlib_len)
+			return false;
+
+		return (dst_ofs == dst_len);
+	}
+	
+	template<uint32_t dst_comps>
+	static bool fpng_pixel_zlib_decompress_3(
+		const uint8_t* pSrc, uint32_t src_len, uint32_t zlib_len,
+		uint8_t* pDst, uint32_t w, uint32_t h)
+	{
+		assert(src_len >= (zlib_len + 4));
+
+		const uint32_t dst_bpl = w * dst_comps;
+		//const uint32_t dst_len = dst_bpl * h;
+
+		if (zlib_len < 7)
+			return false;
+
+		// check zlib header
+		if ((pSrc[0] != 0x78) || (pSrc[1] != 0x01))
+			return false;
+
+		uint32_t src_ofs = 2;
+		
+		if ((pSrc[src_ofs] & 6) == 0)
+			return fpng_pixel_zlib_raw_decompress(pSrc, src_len, zlib_len, pDst, w, h, 3, dst_comps);
+		
+		if ((src_ofs + 4) > src_len)
+			return false;
+		uint64_t bit_buf = READ_LE32(pSrc + src_ofs);
+		src_ofs += 4;
+
+		uint32_t bit_buf_size = 32;
+
+		uint32_t bfinal, btype;
+		GET_BITS(bfinal, 1);
+		GET_BITS(btype, 2);
+
+		// Must be the final block or it's not valid, and type=2 (dynamic)
+		if ((bfinal != 1) || (btype != 2))
+			return false;
+		
+		uint32_t lit_table[FPNG_DECODER_TABLE_SIZE];
+		if (!prepare_dynamic_block(pSrc, src_len, src_ofs, bit_buf_size, bit_buf, lit_table, 3))
+			return false;
+
+		const uint8_t* pPrev_scanline = nullptr;
+		uint8_t* pCur_scanline = pDst;
+
+		for (uint32_t y = 0; y < h; y++)
+		{
+			// At start of PNG scanline, so read the filter literal
+			assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+			uint32_t filter = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+			uint32_t filter_len = (filter >> 9) & 15;
+			if (!filter_len)
+				return false;
+			SKIP_BITS(filter_len);
+			filter &= 511;
+
+			uint32_t expected_filter = (y ? 2 : 0);
+			if (filter != expected_filter)
+				return false;
+
+			uint32_t x_ofs = 0;
+			uint8_t prev_delta_r = 0, prev_delta_g = 0, prev_delta_b = 0;
+			do
+			{
+				assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+				uint32_t lit0_tab = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+				
+				uint32_t lit0 = lit0_tab;
+				uint32_t lit0_len = (lit0_tab >> 9) & 15;
+				if (!lit0_len)
+					return false;
+				SKIP_BITS(lit0_len);
+
+				if (lit0 & 256)
+				{
+					lit0 &= 511;
+
+					// Can't be EOB - we still have more pixels to decompress.
+					if (lit0 == 256)
+						return false;
+
+					// Must be an RLE match against the previous pixel.
+					uint32_t run_len = s_length_range[lit0 - 257];
+					if (lit0 >= 265)
+					{
+						uint32_t e;
+						GET_BITS_NE(e, s_length_extra[lit0 - 257]);
+
+						run_len += e;
+					}
+					
+					// Skip match distance - it's always the same (3)
+					SKIP_BITS_NE(1);
+
+					// Matches must always be a multiple of 3/4 bytes
+					assert((run_len % 3) == 0);
+																				
+					if (dst_comps == 4)
+					{
+						const uint32_t x_ofs_end = x_ofs + g_run_len3_to_4[run_len];
+						
+						// Check for valid run lengths
+						if (x_ofs == x_ofs_end)
+							return false;
+
+						// Matches cannot cross scanlines.
+						if (x_ofs_end > dst_bpl)
+							return false;
+
+						if (pPrev_scanline)
+						{
+							if ((prev_delta_r | prev_delta_g | prev_delta_b) == 0)
+							{
+								memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, x_ofs_end - x_ofs);
+								x_ofs = x_ofs_end;
+							}
+							else
+							{
+								do
+								{
+									pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r);
+									pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g);
+									pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b);
+									pCur_scanline[x_ofs + 3] = 0xFF;
+									x_ofs += 4;
+								} while (x_ofs < x_ofs_end);
+							}
+						}
+						else
+						{
+							do
+							{
+								pCur_scanline[x_ofs] = prev_delta_r;
+								pCur_scanline[x_ofs + 1] = prev_delta_g;
+								pCur_scanline[x_ofs + 2] = prev_delta_b;
+								pCur_scanline[x_ofs + 3] = 0xFF;
+								x_ofs += 4;
+							} while (x_ofs < x_ofs_end);
+						}
+					}
+					else
+					{
+						// Check for valid run lengths
+						if (!g_run_len3_to_4[run_len])
+							return false;
+
+						const uint32_t x_ofs_end = x_ofs + run_len;
+
+						// Matches cannot cross scanlines.
+						if (x_ofs_end > dst_bpl)
+							return false;
+
+						if (pPrev_scanline)
+						{
+							if ((prev_delta_r | prev_delta_g | prev_delta_b) == 0)
+							{
+								memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, run_len);
+								x_ofs = x_ofs_end;
+							}
+							else
+							{
+								do
+								{
+									pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r);
+									pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g);
+									pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b);
+									x_ofs += 3;
+								} while (x_ofs < x_ofs_end);
+							}
+						}
+						else
+						{
+							do
+							{
+								pCur_scanline[x_ofs] = prev_delta_r;
+								pCur_scanline[x_ofs + 1] = prev_delta_g;
+								pCur_scanline[x_ofs + 2] = prev_delta_b;
+								x_ofs += 3;
+							} while (x_ofs < x_ofs_end);
+						}
+					}
+				}
+				else
+				{
+					uint32_t lit1, lit2;
+
+					uint32_t lit1_spec_len = (lit0_tab >> (16 + 9));
+					uint32_t lit2_len;
+					if (lit1_spec_len)
+					{
+						lit1 = (lit0_tab >> 16) & 511;
+						SKIP_BITS_NE(lit1_spec_len);
+
+						assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+						lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+						lit2_len = (lit2 >> 9) & 15;
+						if (!lit2_len)
+							return false;
+					}
+					else
+					{
+						assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+						lit1 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+						uint32_t lit1_len = (lit1 >> 9) & 15;
+						if (!lit1_len)
+							return false;
+						SKIP_BITS_NE(lit1_len);
+
+						lit2_len = (lit1 >> (16 + 9));
+						if (lit2_len)
+							lit2 = lit1 >> 16;
+						else
+						{
+							assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+							lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+							lit2_len = (lit2 >> 9) & 15;
+							if (!lit2_len)
+								return false;
+						}
+					}
+
+					SKIP_BITS(lit2_len);
+					
+					// Check for matches
+					if ((lit1 | lit2) & 256)
+						return false;
+
+					if (dst_comps == 4)
+					{
+						if (pPrev_scanline)
+						{
+							pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0);
+							pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1);
+							pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2);
+							pCur_scanline[x_ofs + 3] = 0xFF;
+						}
+						else
+						{
+							pCur_scanline[x_ofs] = (uint8_t)lit0;
+							pCur_scanline[x_ofs + 1] = (uint8_t)lit1;
+							pCur_scanline[x_ofs + 2] = (uint8_t)lit2;
+							pCur_scanline[x_ofs + 3] = 0xFF;
+						}
+						x_ofs += 4;
+					}
+					else
+					{
+						if (pPrev_scanline)
+						{
+							pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0);
+							pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1);
+							pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2);
+						}
+						else
+						{
+							pCur_scanline[x_ofs] = (uint8_t)lit0;
+							pCur_scanline[x_ofs + 1] = (uint8_t)lit1;
+							pCur_scanline[x_ofs + 2] = (uint8_t)lit2;
+						}
+						x_ofs += 3;
+					}
+
+					prev_delta_r = (uint8_t)lit0;
+					prev_delta_g = (uint8_t)lit1;
+					prev_delta_b = (uint8_t)lit2;
+										
+					// See if we can decode one more pixel.
+					uint32_t spec_next_len0_len = lit2 >> (16 + 9);
+					if ((spec_next_len0_len) && (x_ofs < dst_bpl))
+					{
+						lit0 = (lit2 >> 16) & 511;
+						if (lit0 < 256)
+						{
+							SKIP_BITS_NE(spec_next_len0_len);
+
+							assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+							lit1 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+							uint32_t lit1_len = (lit1 >> 9) & 15;
+							if (!lit1_len)
+								return false;
+							SKIP_BITS(lit1_len);
+
+							lit2_len = (lit1 >> (16 + 9));
+							if (lit2_len)
+								lit2 = lit1 >> 16;
+							else
+							{
+								assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+								lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+								lit2_len = (lit2 >> 9) & 15;
+								if (!lit2_len)
+									return false;
+							}
+
+							SKIP_BITS_NE(lit2_len);
+
+							// Check for matches
+							if ((lit1 | lit2) & 256)
+								return false;
+					
+							if (dst_comps == 4)
+							{
+								if (pPrev_scanline)
+								{
+									pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0);
+									pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1);
+									pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2);
+									pCur_scanline[x_ofs + 3] = 0xFF;
+								}
+								else
+								{
+									pCur_scanline[x_ofs] = (uint8_t)lit0;
+									pCur_scanline[x_ofs + 1] = (uint8_t)lit1;
+									pCur_scanline[x_ofs + 2] = (uint8_t)lit2;
+									pCur_scanline[x_ofs + 3] = 0xFF;
+								}
+								x_ofs += 4;
+							}
+							else
+							{
+								if (pPrev_scanline)
+								{
+									pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0);
+									pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1);
+									pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2);
+								}
+								else
+								{
+									pCur_scanline[x_ofs] = (uint8_t)lit0;
+									pCur_scanline[x_ofs + 1] = (uint8_t)lit1;
+									pCur_scanline[x_ofs + 2] = (uint8_t)lit2;
+								}
+								x_ofs += 3;
+							}
+
+							prev_delta_r = (uint8_t)lit0;
+							prev_delta_g = (uint8_t)lit1;
+							prev_delta_b = (uint8_t)lit2;
+																				
+						} // if (lit0 < 256)
+
+					} // if ((spec_next_len0_len) && (x_ofs < bpl))
+				}
+
+			} while (x_ofs < dst_bpl);
+
+			pPrev_scanline = pCur_scanline;
+			pCur_scanline += dst_bpl;
+
+		} // y
+
+		// The last symbol should be EOB
+		assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+		uint32_t lit0 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+		uint32_t lit0_len = (lit0 >> 9) & 15;
+		if (!lit0_len)
+			return false;
+		lit0 &= 511;
+		if (lit0 != 256)
+			return false;
+
+		bit_buf_size -= lit0_len;
+		bit_buf >>= lit0_len;
+
+		uint32_t align_bits = bit_buf_size & 7;
+		bit_buf_size -= align_bits;
+		bit_buf >>= align_bits;
+
+		if (src_ofs < (bit_buf_size >> 3))
+			return false;
+		src_ofs -= (bit_buf_size >> 3);
+
+		// We should be at the very end, because the bit buf reads ahead 32-bits (which contains the zlib adler32).
+		if ((src_ofs + 4) != zlib_len)
+			return false;
+
+		return true;
+	}
+
+	template<uint32_t dst_comps>
+	static bool fpng_pixel_zlib_decompress_4(
+		const uint8_t* pSrc, uint32_t src_len, uint32_t zlib_len,
+		uint8_t* pDst, uint32_t w, uint32_t h)
+	{
+		assert(src_len >= (zlib_len + 4));
+
+		const uint32_t dst_bpl = w * dst_comps;
+		//const uint32_t dst_len = dst_bpl * h;
+
+		if (zlib_len < 7)
+			return false;
+
+		// check zlib header
+		if ((pSrc[0] != 0x78) || (pSrc[1] != 0x01))
+			return false;
+
+		uint32_t src_ofs = 2;
+
+		if ((pSrc[src_ofs] & 6) == 0)
+			return fpng_pixel_zlib_raw_decompress(pSrc, src_len, zlib_len, pDst, w, h, 4, dst_comps);
+
+		if ((src_ofs + 4) > src_len)
+			return false;
+		uint64_t bit_buf = READ_LE32(pSrc + src_ofs);
+		src_ofs += 4;
+
+		uint32_t bit_buf_size = 32;
+
+		uint32_t bfinal, btype;
+		GET_BITS(bfinal, 1);
+		GET_BITS(btype, 2);
+
+		// Must be the final block or it's not valid, and type=2 (dynamic)
+		if ((bfinal != 1) || (btype != 2))
+			return false;
+
+		uint32_t lit_table[FPNG_DECODER_TABLE_SIZE];
+		if (!prepare_dynamic_block(pSrc, src_len, src_ofs, bit_buf_size, bit_buf, lit_table, 4))
+			return false;
+
+		const uint8_t* pPrev_scanline = nullptr;
+		uint8_t* pCur_scanline = pDst;
+
+		for (uint32_t y = 0; y < h; y++)
+		{
+			// At start of PNG scanline, so read the filter literal
+			assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+			uint32_t filter = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+			uint32_t filter_len = (filter >> 9) & 15;
+			if (!filter_len)
+				return false;
+			SKIP_BITS(filter_len);
+			filter &= 511;
+
+			uint32_t expected_filter = (y ? 2 : 0);
+			if (filter != expected_filter)
+				return false;
+
+			uint32_t x_ofs = 0;
+			uint8_t prev_delta_r = 0, prev_delta_g = 0, prev_delta_b = 0, prev_delta_a = 0;
+			do
+			{
+				assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+				uint32_t lit0_tab = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+
+				uint32_t lit0 = lit0_tab;
+				uint32_t lit0_len = (lit0_tab >> 9) & 15;
+				if (!lit0_len)
+					return false;
+				SKIP_BITS(lit0_len);
+
+				if (lit0 & 256)
+				{
+					lit0 &= 511;
+
+					// Can't be EOB - we still have more pixels to decompress.
+					if (lit0 == 256)
+						return false;
+
+					// Must be an RLE match against the previous pixel.
+					uint32_t run_len = s_length_range[lit0 - 257];
+					if (lit0 >= 265)
+					{
+						uint32_t e;
+						GET_BITS_NE(e, s_length_extra[lit0 - 257]);
+
+						run_len += e;
+					}
+
+					// Skip match distance - it's always the same (4)
+					SKIP_BITS_NE(1);
+
+					// Matches must always be a multiple of 3/4 bytes
+					if (run_len & 3)
+						return false;
+										
+					if (dst_comps == 3)
+					{
+						const uint32_t run_len3 = (run_len >> 2) * 3;
+						const uint32_t x_ofs_end = x_ofs + run_len3;
+
+						// Matches cannot cross scanlines.
+						if (x_ofs_end > dst_bpl)
+							return false;
+
+						if (pPrev_scanline)
+						{
+							if ((prev_delta_r | prev_delta_g | prev_delta_b | prev_delta_a) == 0)
+							{
+								memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, run_len3);
+								x_ofs = x_ofs_end;
+							}
+							else
+							{
+								do
+								{
+									pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r);
+									pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g);
+									pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b);
+									x_ofs += 3;
+								} while (x_ofs < x_ofs_end);
+							}
+						}
+						else
+						{
+							do
+							{
+								pCur_scanline[x_ofs] = prev_delta_r;
+								pCur_scanline[x_ofs + 1] = prev_delta_g;
+								pCur_scanline[x_ofs + 2] = prev_delta_b;
+								x_ofs += 3;
+							} while (x_ofs < x_ofs_end);
+						}
+					}
+					else
+					{
+						const uint32_t x_ofs_end = x_ofs + run_len;
+
+						// Matches cannot cross scanlines.
+						if (x_ofs_end > dst_bpl)
+							return false;
+
+						if (pPrev_scanline)
+						{
+							if ((prev_delta_r | prev_delta_g | prev_delta_b | prev_delta_a) == 0)
+							{
+								memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, run_len);
+								x_ofs = x_ofs_end;
+							}
+							else
+							{
+								do
+								{
+									pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r);
+									pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g);
+									pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b);
+									pCur_scanline[x_ofs + 3] = (uint8_t)(pPrev_scanline[x_ofs + 3] + prev_delta_a);
+									x_ofs += 4;
+								} while (x_ofs < x_ofs_end);
+							}
+						}
+						else
+						{
+							do
+							{
+								pCur_scanline[x_ofs] = prev_delta_r;
+								pCur_scanline[x_ofs + 1] = prev_delta_g;
+								pCur_scanline[x_ofs + 2] = prev_delta_b;
+								pCur_scanline[x_ofs + 3] = prev_delta_a;
+								x_ofs += 4;
+							} while (x_ofs < x_ofs_end);
+						}
+					}
+				}
+				else
+				{
+					uint32_t lit1, lit2;
+
+					uint32_t lit1_spec_len = (lit0_tab >> (16 + 9));
+					uint32_t lit2_len;
+					if (lit1_spec_len)
+					{
+						lit1 = (lit0_tab >> 16) & 511;
+						SKIP_BITS_NE(lit1_spec_len);
+
+						assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+						lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+						lit2_len = (lit2 >> 9) & 15;
+						if (!lit2_len)
+							return false;
+					}
+					else
+					{
+						assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+						lit1 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+						uint32_t lit1_len = (lit1 >> 9) & 15;
+						if (!lit1_len)
+							return false;
+						SKIP_BITS_NE(lit1_len);
+
+						lit2_len = (lit1 >> (16 + 9));
+						if (lit2_len)
+							lit2 = lit1 >> 16;
+						else
+						{
+							assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+							lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+							lit2_len = (lit2 >> 9) & 15;
+							if (!lit2_len)
+								return false;
+						}
+					}
+
+					uint32_t lit3;
+					uint32_t lit3_len = lit2 >> (16 + 9);
+					
+					if (lit3_len)
+					{
+						lit3 = (lit2 >> 16);
+						SKIP_BITS(lit2_len + lit3_len);
+					}
+					else
+					{
+						SKIP_BITS(lit2_len);
+
+						assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+						lit3 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+						lit3_len = (lit3 >> 9) & 15;
+						if (!lit3_len)
+							return false;
+
+						SKIP_BITS_NE(lit3_len);
+					}
+										
+					// Check for matches
+					if ((lit1 | lit2 | lit3) & 256)
+						return false;
+
+					if (dst_comps == 3)
+					{
+						if (pPrev_scanline)
+						{
+							pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0);
+							pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1);
+							pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2);
+						}
+						else
+						{
+							pCur_scanline[x_ofs] = (uint8_t)lit0;
+							pCur_scanline[x_ofs + 1] = (uint8_t)lit1;
+							pCur_scanline[x_ofs + 2] = (uint8_t)lit2;
+						}
+
+						x_ofs += 3;
+					}
+					else
+					{
+						if (pPrev_scanline)
+						{
+							pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0);
+							pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1);
+							pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2);
+							pCur_scanline[x_ofs + 3] = (uint8_t)(pPrev_scanline[x_ofs + 3] + lit3);
+						}
+						else
+						{
+							pCur_scanline[x_ofs] = (uint8_t)lit0;
+							pCur_scanline[x_ofs + 1] = (uint8_t)lit1;
+							pCur_scanline[x_ofs + 2] = (uint8_t)lit2;
+							pCur_scanline[x_ofs + 3] = (uint8_t)lit3;
+						}
+						
+						x_ofs += 4;
+					}
+
+					prev_delta_r = (uint8_t)lit0;
+					prev_delta_g = (uint8_t)lit1;
+					prev_delta_b = (uint8_t)lit2;
+					prev_delta_a = (uint8_t)lit3;
+				}
+
+			} while (x_ofs < dst_bpl);
+
+			pPrev_scanline = pCur_scanline;
+			pCur_scanline += dst_bpl;
+		} // y
+
+		// The last symbol should be EOB
+		assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS);
+		uint32_t lit0 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)];
+		uint32_t lit0_len = (lit0 >> 9) & 15;
+		if (!lit0_len)
+			return false;
+		lit0 &= 511;
+		if (lit0 != 256)
+			return false;
+
+		bit_buf_size -= lit0_len;
+		bit_buf >>= lit0_len;
+
+		uint32_t align_bits = bit_buf_size & 7;
+		bit_buf_size -= align_bits;
+		bit_buf >>= align_bits;
+
+		if (src_ofs < (bit_buf_size >> 3))
+			return false;
+		src_ofs -= (bit_buf_size >> 3);
+
+		// We should be at the very end, because the bit buf reads ahead 32-bits (which contains the zlib adler32).
+		if ((src_ofs + 4) != zlib_len)
+			return false;
+
+		return true;
+	}
+
+#pragma pack(push)
+#pragma pack(1)
+	struct png_chunk_prefix
+	{
+		uint32_t m_length;
+		uint8_t m_type[4];
+	};
+	struct png_ihdr
+	{
+		png_chunk_prefix m_prefix;
+		uint32_t m_width;
+		uint32_t m_height;
+		uint8_t m_bitdepth;
+		uint8_t m_color_type;
+		uint8_t m_comp_method;
+		uint8_t m_filter_method;
+		uint8_t m_interlace_method;
+		uint32_t m_crc32;
+	};
+	const uint32_t IHDR_EXPECTED_LENGTH = 13;
+	struct png_iend
+	{
+		png_chunk_prefix m_prefix;
+		uint32_t m_crc32;
+	};
+#pragma pack(pop)
+
+	static int fpng_get_info_internal(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t &idat_ofs, uint32_t &idat_len)
+	{
+		static const uint8_t s_png_sig[8] = { 137, 80, 78, 71, 13, 10, 26, 10 };
+
+		if (!endian_check())
+		{
+			assert(0);
+			return false;
+		}
+				
+		width = 0;
+		height = 0;
+		channels_in_file = 0;
+		idat_ofs = 0, idat_len = 0;
+				
+		// Ensure the file has at least a minimum possible size
+		if (image_size < (sizeof(s_png_sig) + sizeof(png_ihdr) + sizeof(png_chunk_prefix) + 1 + sizeof(uint32_t) + sizeof(png_iend)))
+			return FPNG_DECODE_FAILED_NOT_PNG;
+
+		if (memcmp(pImage, s_png_sig, 8) != 0)
+			return FPNG_DECODE_FAILED_NOT_PNG;
+
+		const uint8_t* pImage_u8 = static_cast<const uint8_t*>(pImage) + 8;
+
+		const png_ihdr& ihdr = *reinterpret_cast<const png_ihdr*>(pImage_u8);
+		pImage_u8 += sizeof(png_ihdr);
+
+		if (READ_BE32(&ihdr.m_prefix.m_length) != IHDR_EXPECTED_LENGTH)
+			return FPNG_DECODE_FAILED_NOT_PNG;
+
+		if (fpng_crc32(ihdr.m_prefix.m_type, 4 + IHDR_EXPECTED_LENGTH, FPNG_CRC32_INIT) != READ_BE32(&ihdr.m_crc32))
+			return FPNG_DECODE_FAILED_HEADER_CRC32;
+
+		width = READ_BE32(&ihdr.m_width);
+		height = READ_BE32(&ihdr.m_height);
+				
+		if (!width || !height || (width > FPNG_MAX_SUPPORTED_DIM) || (height > FPNG_MAX_SUPPORTED_DIM))
+			return FPNG_DECODE_FAILED_INVALID_DIMENSIONS;
+
+		uint64_t total_pixels = (uint64_t)width * height;
+		if (total_pixels > (1 << 30))
+			return FPNG_DECODE_FAILED_INVALID_DIMENSIONS;
+
+		if ((ihdr.m_comp_method) || (ihdr.m_filter_method) || (ihdr.m_interlace_method) || (ihdr.m_bitdepth != 8))
+			return FPNG_DECODE_NOT_FPNG;
+
+		if (ihdr.m_color_type == 2)
+			channels_in_file = 3;
+		else if (ihdr.m_color_type == 6)
+			channels_in_file = 4;
+
+		if (!channels_in_file)
+			return FPNG_DECODE_NOT_FPNG;
+
+		// Scan all the chunks. Look for one IDAT, IEND, and our custom fdEC chunk that indicates the file was compressed by us. Skip any ancillary chunks.
+		bool found_fdec_chunk = false;
+		
+		for (; ; )
+		{
+			const size_t src_ofs = pImage_u8 - static_cast<const uint8_t*>(pImage);
+			if (src_ofs >= image_size)
+				return FPNG_DECODE_FAILED_CHUNK_PARSING;
+
+			const uint32_t bytes_remaining = image_size - (uint32_t)src_ofs;
+			if (bytes_remaining < sizeof(uint32_t) * 3)
+				return FPNG_DECODE_FAILED_CHUNK_PARSING;
+
+			const png_chunk_prefix* pChunk = reinterpret_cast<const png_chunk_prefix*>(pImage_u8);
+
+			const uint32_t chunk_len = READ_BE32(&pChunk->m_length);
+			if ((src_ofs + sizeof(uint32_t) + chunk_len + sizeof(uint32_t)) > image_size)
+				return FPNG_DECODE_FAILED_CHUNK_PARSING;
+
+			for (uint32_t i = 0; i < 4; i++)
+			{
+				const uint8_t c = pChunk->m_type[i];
+				const bool is_upper = (c >= 65) && (c <= 90), is_lower = (c >= 97) && (c <= 122);
+				if ((!is_upper) && (!is_lower))
+					return FPNG_DECODE_FAILED_CHUNK_PARSING;
+			}
+
+			const uint32_t expected_crc32 = READ_BE32(pImage_u8 + sizeof(uint32_t) * 2 + chunk_len);
+
+			char chunk_type[5] = { (char)pChunk->m_type[0], (char)pChunk->m_type[1], (char)pChunk->m_type[2], (char)pChunk->m_type[3], 0 };
+			const bool is_idat = strcmp(chunk_type, "IDAT") == 0;
+
+#if !FPNG_DISABLE_DECODE_CRC32_CHECKS
+			if (!is_idat)
+			{
+				uint32_t actual_crc32 = fpng_crc32(pImage_u8 + sizeof(uint32_t), sizeof(uint32_t) + chunk_len, FPNG_CRC32_INIT);
+				if (actual_crc32 != expected_crc32)
+					return FPNG_DECODE_FAILED_HEADER_CRC32;
+			}
+#endif
+
+			const uint8_t* pChunk_data = pImage_u8 + sizeof(uint32_t) * 2;
+
+			if (strcmp(chunk_type, "IEND") == 0)
+				break;
+			else if (is_idat)
+			{
+				// If there were multiple IDAT's, or we didn't find the fdEC chunk, then it's not FPNG.
+				if ((idat_ofs) || (!found_fdec_chunk))
+					return FPNG_DECODE_NOT_FPNG;
+
+				idat_ofs = (uint32_t)src_ofs;
+				idat_len = chunk_len;
+
+				// Sanity check the IDAT chunk length
+				if (idat_len < 7)
+					return FPNG_DECODE_FAILED_INVALID_IDAT;
+			}
+			else if (strcmp(chunk_type, "fdEC") == 0)
+			{
+				if (found_fdec_chunk)
+					return FPNG_DECODE_NOT_FPNG;
+
+				// We've got our fdEC chunk. Now make sure it's big enough and check its contents.
+				if (chunk_len != 5)
+					return FPNG_DECODE_NOT_FPNG;
+
+				// Check fdEC chunk sig
+				if ((pChunk_data[0] != 82) || (pChunk_data[1] != 36) || (pChunk_data[2] != 147) || (pChunk_data[3] != 227))
+					return FPNG_DECODE_NOT_FPNG;
+
+				// Check fdEC version
+				if (pChunk_data[4] != FPNG_FDEC_VERSION)
+					return FPNG_DECODE_NOT_FPNG;
+
+				found_fdec_chunk = true;
+			}
+			else
+			{
+				// Bail if it's a critical chunk - can't be FPNG
+				if ((chunk_type[0] & 32) == 0)
+					return FPNG_DECODE_NOT_FPNG;
+
+				// ancillary chunk - skip it
+			}
+
+			pImage_u8 += sizeof(png_chunk_prefix) + chunk_len + sizeof(uint32_t);
+		}
+
+		if ((!found_fdec_chunk) || (!idat_ofs))
+			return FPNG_DECODE_NOT_FPNG;
+		
+		return FPNG_DECODE_SUCCESS;
+	}
+
+	int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file)
+	{
+		uint32_t idat_ofs = 0, idat_len = 0;
+		return fpng_get_info_internal(pImage, image_size, width, height, channels_in_file, idat_ofs, idat_len);
+	}
+
+	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)
+	{
+		out.resize(0);
+		width = 0;
+		height = 0;
+		channels_in_file = 0;
+
+		if ((!pImage) || (!image_size) || ((desired_channels != 3) && (desired_channels != 4)))
+		{
+			assert(0);
+			return FPNG_DECODE_INVALID_ARG;
+		}
+
+		uint32_t idat_ofs = 0, idat_len = 0;
+		int status = fpng_get_info_internal(pImage, image_size, width, height, channels_in_file, idat_ofs, idat_len);
+		if (status)
+			return status;
+				
+		const uint64_t mem_needed = (uint64_t)width * height * desired_channels;
+		if (mem_needed > UINT32_MAX)
+			return FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE;
+
+		// On 32-bit systems do a quick sanity check before we try to resize the output buffer.
+		if ((sizeof(size_t) == sizeof(uint32_t)) && (mem_needed >= 0x80000000))
+			return FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE;
+
+		out.resize(mem_needed);
+		
+		const uint8_t* pIDAT_data = static_cast<const uint8_t*>(pImage) + idat_ofs + sizeof(uint32_t) * 2;
+		const uint32_t src_len = image_size - (idat_ofs + sizeof(uint32_t) * 2);
+
+		bool decomp_status;
+		if (desired_channels == 3)
+		{
+			if (channels_in_file == 3)
+				decomp_status = fpng_pixel_zlib_decompress_3<3>(pIDAT_data, src_len, idat_len, out.data(), width, height);
+			else
+				decomp_status = fpng_pixel_zlib_decompress_4<3>(pIDAT_data, src_len, idat_len, out.data(), width, height);
+		}
+		else
+		{
+			if (channels_in_file == 3)
+				decomp_status = fpng_pixel_zlib_decompress_3<4>(pIDAT_data, src_len, idat_len, out.data(), width, height);
+			else
+				decomp_status = fpng_pixel_zlib_decompress_4<4>(pIDAT_data, src_len, idat_len, out.data(), width, height);
+		}
+		if (!decomp_status)
+		{
+			// Something went wrong. Either the file data was corrupted, or it doesn't conform to one of our zlib/Deflate constraints.
+			// The conservative thing to do is indicate it wasn't written by us, and let the general purpose PNG decoder handle it.
+			return FPNG_DECODE_NOT_FPNG;
+		}
+
+		return FPNG_DECODE_SUCCESS;
+	}
+
+#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)
+	{
+		FILE* pFile = nullptr;
+
+#ifdef _MSC_VER
+		fopen_s(&pFile, pFilename, "rb");
+#else
+		pFile = fopen(pFilename, "rb");
+#endif
+
+		if (!pFile)
+			return FPNG_DECODE_FILE_OPEN_FAILED;
+
+		if (fseek(pFile, 0, SEEK_END) != 0)
+		{
+			fclose(pFile);
+			return FPNG_DECODE_FILE_SEEK_FAILED;
+		}
+
+#ifdef _WIN32
+		int64_t filesize = _ftelli64(pFile);
+#else
+		int64_t filesize = ftello(pFile);
+#endif
+
+		if (fseek(pFile, 0, SEEK_SET) != 0)
+		{
+			fclose(pFile);
+			return FPNG_DECODE_FILE_SEEK_FAILED;
+		}
+
+		if ( (filesize < 0) || (filesize > UINT32_MAX) || ( (sizeof(size_t) == sizeof(uint32_t)) && (filesize > 0x70000000) ) )
+		{
+			fclose(pFile);
+			return FPNG_DECODE_FILE_TOO_LARGE;
+		}
+
+		std::vector<uint8_t> buf((size_t)filesize);
+		if (fread(buf.data(), 1, buf.size(), pFile) != buf.size())
+		{
+			fclose(pFile);
+			return FPNG_DECODE_FILE_READ_FAILED;
+		}
+
+		fclose(pFile);
+
+		return fpng_decode_memory(buf.data(), (uint32_t)buf.size(), out, width, height, channels_in_file, desired_channels);
+	}
+#endif
+
+} // namespace fpng
+
+/*
+	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/>
+
+	Richard Geldreich, Jr.
+	12/30/2021
+*/
diff --git a/ui/thirdparty/fpng/fpng.h b/ui/thirdparty/fpng/fpng.h
new file mode 100644
index 0000000000..4d55e3afde
--- /dev/null
+++ b/ui/thirdparty/fpng/fpng.h
@@ -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
diff --git a/ui/httplib.h b/ui/thirdparty/httplib/httplib.h
similarity index 100%
rename from ui/httplib.h
rename to ui/thirdparty/httplib/httplib.h
diff --git a/ui/thirdparty/imgui b/ui/thirdparty/imgui
new file mode 160000
index 0000000000..c71a50deb5
--- /dev/null
+++ b/ui/thirdparty/imgui
@@ -0,0 +1 @@
+Subproject commit c71a50deb5ddf1ea386b91e60fa2e4a26d080074
diff --git a/ui/thirdparty/imgui_impl_opengl3_loader_override.h b/ui/thirdparty/imgui_impl_opengl3_loader_override.h
new file mode 100644
index 0000000000..2daa135837
--- /dev/null
+++ b/ui/thirdparty/imgui_impl_opengl3_loader_override.h
@@ -0,0 +1 @@
+#include <epoxy/gl.h>
diff --git a/ui/thirdparty/implot b/ui/thirdparty/implot
new file mode 160000
index 0000000000..b47c8bacdb
--- /dev/null
+++ b/ui/thirdparty/implot
@@ -0,0 +1 @@
+Subproject commit b47c8bacdbc78bc521691f70666f13924bb522ab
diff --git a/ui/json.hpp b/ui/thirdparty/json/json.hpp
similarity index 100%
rename from ui/json.hpp
rename to ui/thirdparty/json/json.hpp
diff --git a/ui/thirdparty/meson.build b/ui/thirdparty/meson.build
new file mode 100644
index 0000000000..677283bdb5
--- /dev/null
+++ b/ui/thirdparty/meson.build
@@ -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')
diff --git a/ui/noc_file_dialog.h b/ui/thirdparty/noc_file_dialog/noc_file_dialog.h
similarity index 99%
rename from ui/noc_file_dialog.h
rename to ui/thirdparty/noc_file_dialog/noc_file_dialog.h
index 93ab4a9c8a..c72bf8330a 100644
--- a/ui/noc_file_dialog.h
+++ b/ui/thirdparty/noc_file_dialog/noc_file_dialog.h
@@ -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
diff --git a/ui/noc_file_dialog_gtk.c b/ui/thirdparty/noc_file_dialog/noc_file_dialog_gtk.c
similarity index 100%
rename from ui/noc_file_dialog_gtk.c
rename to ui/thirdparty/noc_file_dialog/noc_file_dialog_gtk.c
diff --git a/ui/noc_file_dialog_macos.m b/ui/thirdparty/noc_file_dialog/noc_file_dialog_macos.m
similarity index 100%
rename from ui/noc_file_dialog_macos.m
rename to ui/thirdparty/noc_file_dialog/noc_file_dialog_macos.m
diff --git a/ui/noc_file_dialog_win32.c b/ui/thirdparty/noc_file_dialog/noc_file_dialog_win32.c
similarity index 100%
rename from ui/noc_file_dialog_win32.c
rename to ui/thirdparty/noc_file_dialog/noc_file_dialog_win32.c
diff --git a/ui/stb_image.h b/ui/thirdparty/stb_image/stb_image.h
similarity index 93%
rename from ui/stb_image.h
rename to ui/thirdparty/stb_image/stb_image.h
index 2857f05d38..d60371b95f 100644
--- a/ui/stb_image.h
+++ b/ui/thirdparty/stb_image/stb_image.h
@@ -1,4 +1,4 @@
-/* stb_image - v2.25 - public domain image loader - http://nothings.org/stb
+/* stb_image - v2.27 - public domain image loader - http://nothings.org/stb
                                   no warranty implied; use at your own risk
 
    Do this:
@@ -48,6 +48,8 @@ LICENSE
 
 RECENT REVISION HISTORY:
 
+      2.27  (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes
+      2.26  (2020-07-13) many minor fixes
       2.25  (2020-02-02) fix warnings
       2.24  (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically
       2.23  (2019-08-11) fix clang static analysis warning
@@ -88,27 +90,37 @@ RECENT REVISION HISTORY:
                                            Jeremy Sawicki (handle all ImageNet JPGs)
  Optimizations & bugfixes                  Mikhail Morozov (1-bit BMP)
     Fabian "ryg" Giesen                    Anael Seghezzi (is-16-bit query)
-    Arseny Kapoulkine
+    Arseny Kapoulkine                      Simon Breuss (16-bit PNM)
     John-Mark Allen
     Carmelo J Fdez-Aguera
 
  Bug & warning fixes
-    Marc LeBlanc            David Woo          Guillaume George   Martins Mozeiko
-    Christpher Lloyd        Jerry Jansson      Joseph Thomson     Phil Jordan
-    Dave Moore              Roy Eltham         Hayaki Saito       Nathan Reed
-    Won Chun                Luke Graham        Johan Duparc       Nick Verigakis
-    the Horde3D community   Thomas Ruf         Ronny Chevalier    github:rlyeh
-    Janez Zemva             John Bartholomew   Michal Cichon      github:romigrou
-    Jonathan Blow           Ken Hamada         Tero Hanninen      github:svdijk
-    Laurent Gomila          Cort Stratton      Sergio Gonzalez    github:snagar
-    Aruelien Pocheville     Thibault Reuille   Cass Everitt       github:Zelex
-    Ryamond Barbiero        Paul Du Bois       Engin Manap        github:grim210
-    Aldo Culquicondor       Philipp Wiesemann  Dale Weiler        github:sammyhw
-    Oriol Ferrer Mesia      Josh Tobin         Matthew Gregan     github:phprus
-    Julian Raschke          Gregory Mullen     Baldur Karlsson    github:poppolopoppo
-    Christian Floisand      Kevin Schmidt      JR Smith           github:darealshinji
-    Brad Weinberger         Matvey Cherevko                       github:Michaelangel007
-    Blazej Dariusz Roszkowski                  Alexander Veselov
+    Marc LeBlanc            David Woo          Guillaume George     Martins Mozeiko
+    Christpher Lloyd        Jerry Jansson      Joseph Thomson       Blazej Dariusz Roszkowski
+    Phil Jordan                                Dave Moore           Roy Eltham
+    Hayaki Saito            Nathan Reed        Won Chun
+    Luke Graham             Johan Duparc       Nick Verigakis       the Horde3D community
+    Thomas Ruf              Ronny Chevalier                         github:rlyeh
+    Janez Zemva             John Bartholomew   Michal Cichon        github:romigrou
+    Jonathan Blow           Ken Hamada         Tero Hanninen        github:svdijk
+    Eugene Golushkov        Laurent Gomila     Cort Stratton        github:snagar
+    Aruelien Pocheville     Sergio Gonzalez    Thibault Reuille     github:Zelex
+    Cass Everitt            Ryamond Barbiero                        github:grim210
+    Paul Du Bois            Engin Manap        Aldo Culquicondor    github:sammyhw
+    Philipp Wiesemann       Dale Weiler        Oriol Ferrer Mesia   github:phprus
+    Josh Tobin                                 Matthew Gregan       github:poppolopoppo
+    Julian Raschke          Gregory Mullen     Christian Floisand   github:darealshinji
+    Baldur Karlsson         Kevin Schmidt      JR Smith             github:Michaelangel007
+                            Brad Weinberger    Matvey Cherevko      github:mosra
+    Luca Sas                Alexander Veselov  Zack Middleton       [reserved]
+    Ryan C. Gordon          [reserved]                              [reserved]
+                     DO NOT ADD YOUR NAME HERE
+
+                     Jacko Dirks
+
+  To add your name to the credits, pick a random blank space in the middle and fill it.
+  80% of merge conflicts on stb PRs are due to people adding their name at the end
+  of the credits.
 */
 
 #ifndef STBI_INCLUDE_STB_IMAGE_H
@@ -167,6 +179,32 @@ RECENT REVISION HISTORY:
 //
 // Paletted PNG, BMP, GIF, and PIC images are automatically depalettized.
 //
+// To query the width, height and component count of an image without having to
+// decode the full file, you can use the stbi_info family of functions:
+//
+//   int x,y,n,ok;
+//   ok = stbi_info(filename, &x, &y, &n);
+//   // returns ok=1 and sets x, y, n if image is a supported format,
+//   // 0 otherwise.
+//
+// Note that stb_image pervasively uses ints in its public API for sizes,
+// including sizes of memory buffers. This is now part of the API and thus
+// hard to change without causing breakage. As a result, the various image
+// loaders all have certain limits on image size; these differ somewhat
+// by format but generally boil down to either just under 2GB or just under
+// 1GB. When the decoded image would be larger than this, stb_image decoding
+// will fail.
+//
+// Additionally, stb_image will reject image files that have any of their
+// dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS,
+// which defaults to 2**24 = 16777216 pixels. Due to the above memory limit,
+// the only way to have an image with such dimensions load correctly
+// is for it to have a rather extreme aspect ratio. Either way, the
+// assumption here is that such larger images are likely to be malformed
+// or malicious. If you do need to load an image with individual dimensions
+// larger than that, and it still fits in the overall size limit, you can
+// #define STBI_MAX_DIMENSIONS on your own to be something larger.
+//
 // ===========================================================================
 //
 // UNICODE:
@@ -272,11 +310,10 @@ RECENT REVISION HISTORY:
 //
 // iPhone PNG support:
 //
-// By default we convert iphone-formatted PNGs back to RGB, even though
-// they are internally encoded differently. You can disable this conversion
-// by calling stbi_convert_iphone_png_to_rgb(0), in which case
-// you will always just get the native iphone "format" through (which
-// is BGR stored in RGB).
+// We optionally support converting iPhone-formatted PNGs (which store
+// premultiplied BGRA) back to RGB, even though they're internally encoded
+// differently. To enable this conversion, call
+// stbi_convert_iphone_png_to_rgb(1).
 //
 // Call stbi_set_unpremultiply_on_load(1) as well to force a divide per
 // pixel to remove any premultiplied alpha *only* if the image file explicitly
@@ -318,7 +355,14 @@ RECENT REVISION HISTORY:
 //   - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still
 //     want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB
 //
-
+//  - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater
+//    than that size (in either width or height) without further processing.
+//    This is to let programs in the wild set an upper bound to prevent
+//    denial-of-service attacks on untrusted data, as one could generate a
+//    valid image of gigantic dimensions and force stb_image to allocate a
+//    huge block of memory and spend disproportionate time decoding it. By
+//    default this is set to (1 << 24), which is 16777216, but that's still
+//    very big.
 
 #ifndef STBI_NO_STDIO
 #include <stdio.h>
@@ -473,6 +517,8 @@ STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip);
 // as above, but only applies to images loaded on the thread that calls the function
 // this function is only available if your compiler supports thread-local variables;
 // calling it will fail to link if your compiler doesn't
+STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply);
+STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert);
 STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip);
 
 // ZLIB client - used by PNG, available for other purposes
@@ -574,13 +620,19 @@ STBIDEF int   stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const ch
 #ifndef STBI_NO_THREAD_LOCALS
    #if defined(__cplusplus) &&  __cplusplus >= 201103L
       #define STBI_THREAD_LOCAL       thread_local
-   #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
-      #define STBI_THREAD_LOCAL       _Thread_local
-   #elif defined(__GNUC__)
+   #elif defined(__GNUC__) && __GNUC__ < 5
       #define STBI_THREAD_LOCAL       __thread
    #elif defined(_MSC_VER)
       #define STBI_THREAD_LOCAL       __declspec(thread)
-#endif
+   #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__)
+      #define STBI_THREAD_LOCAL       _Thread_local
+   #endif
+
+   #ifndef STBI_THREAD_LOCAL
+      #if defined(__GNUC__)
+        #define STBI_THREAD_LOCAL       __thread
+      #endif
+   #endif
 #endif
 
 #ifdef _MSC_VER
@@ -612,7 +664,7 @@ typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1];
 #ifdef STBI_HAS_LROTL
    #define stbi_lrot(x,y)  _lrotl(x,y)
 #else
-   #define stbi_lrot(x,y)  (((x) << (y)) | ((x) >> (32 - (y))))
+   #define stbi_lrot(x,y)  (((x) << (y)) | ((x) >> (-(y) & 31)))
 #endif
 
 #if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED))
@@ -726,14 +778,21 @@ static int stbi__sse2_available(void)
 
 #ifdef STBI_NEON
 #include <arm_neon.h>
-// assume GCC or Clang on ARM targets
+#ifdef _MSC_VER
+#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name
+#else
 #define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16)))
 #endif
+#endif
 
 #ifndef STBI_SIMD_ALIGN
 #define STBI_SIMD_ALIGN(type, name) type name
 #endif
 
+#ifndef STBI_MAX_DIMENSIONS
+#define STBI_MAX_DIMENSIONS (1 << 24)
+#endif
+
 ///////////////////////////////////////////////
 //
 //  stbi__context struct and start_xxx functions
@@ -751,6 +810,7 @@ typedef struct
    int read_from_callbacks;
    int buflen;
    stbi_uc buffer_start[128];
+   int callback_already_read;
 
    stbi_uc *img_buffer, *img_buffer_end;
    stbi_uc *img_buffer_original, *img_buffer_original_end;
@@ -764,6 +824,7 @@ static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len)
 {
    s->io.read = NULL;
    s->read_from_callbacks = 0;
+   s->callback_already_read = 0;
    s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer;
    s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len;
 }
@@ -775,7 +836,8 @@ static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *
    s->io_user_data = user;
    s->buflen = sizeof(s->buffer_start);
    s->read_from_callbacks = 1;
-   s->img_buffer_original = s->buffer_start;
+   s->callback_already_read = 0;
+   s->img_buffer = s->img_buffer_original = s->buffer_start;
    stbi__refill_buffer(s);
    s->img_buffer_original_end = s->img_buffer_end;
 }
@@ -789,12 +851,17 @@ static int stbi__stdio_read(void *user, char *data, int size)
 
 static void stbi__stdio_skip(void *user, int n)
 {
+   int ch;
    fseek((FILE*) user, n, SEEK_CUR);
+   ch = fgetc((FILE*) user);  /* have to read a byte to reset feof()'s flag */
+   if (ch != EOF) {
+      ungetc(ch, (FILE *) user);  /* push byte back onto stream if valid. */
+   }
 }
 
 static int stbi__stdio_eof(void *user)
 {
-   return feof((FILE*) user);
+   return feof((FILE*) user) || ferror((FILE *) user);
 }
 
 static stbi_io_callbacks stbi__stdio_callbacks =
@@ -890,6 +957,7 @@ static int      stbi__gif_info(stbi__context *s, int *x, int *y, int *comp);
 static int      stbi__pnm_test(stbi__context *s);
 static void    *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);
 static int      stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp);
+static int      stbi__pnm_is16(stbi__context *s);
 #endif
 
 static
@@ -964,7 +1032,7 @@ static int stbi__mad3sizes_valid(int a, int b, int c, int add)
 }
 
 // returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't overflow
-#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR)
+#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM)
 static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add)
 {
    return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) &&
@@ -987,7 +1055,7 @@ static void *stbi__malloc_mad3(int a, int b, int c, int add)
    return stbi__malloc(a*b*c + add);
 }
 
-#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR)
+#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM)
 static void *stbi__malloc_mad4(int a, int b, int c, int d, int add)
 {
    if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL;
@@ -1053,9 +1121,8 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re
    ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order
    ri->num_channels = 0;
 
-   #ifndef STBI_NO_JPEG
-   if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);
-   #endif
+   // test the formats with a very explicit header first (at least a FOURCC
+   // or distinctive magic number first)
    #ifndef STBI_NO_PNG
    if (stbi__png_test(s))  return stbi__png_load(s,x,y,comp,req_comp, ri);
    #endif
@@ -1073,6 +1140,13 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re
    #ifndef STBI_NO_PIC
    if (stbi__pic_test(s))  return stbi__pic_load(s,x,y,comp,req_comp, ri);
    #endif
+
+   // then the formats that can end up attempting to load with just 1 or 2
+   // bytes matching expectations; these are prone to false positives, so
+   // try them later
+   #ifndef STBI_NO_JPEG
+   if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);
+   #endif
    #ifndef STBI_NO_PNM
    if (stbi__pnm_test(s))  return stbi__pnm_load(s,x,y,comp,req_comp, ri);
    #endif
@@ -1171,8 +1245,10 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x,
    if (result == NULL)
       return NULL;
 
+   // it is the responsibility of the loaders to make sure we get either 8 or 16 bit.
+   STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16);
+
    if (ri.bits_per_channel != 8) {
-      STBI_ASSERT(ri.bits_per_channel == 16);
       result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp);
       ri.bits_per_channel = 8;
    }
@@ -1195,8 +1271,10 @@ static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x,
    if (result == NULL)
       return NULL;
 
+   // it is the responsibility of the loaders to make sure we get either 8 or 16 bit.
+   STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16);
+
    if (ri.bits_per_channel != 16) {
-      STBI_ASSERT(ri.bits_per_channel == 8);
       result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp);
       ri.bits_per_channel = 16;
    }
@@ -1224,12 +1302,12 @@ static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, in
 
 #ifndef STBI_NO_STDIO
 
-#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8)
+#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8)
 STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide);
 STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default);
 #endif
 
-#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8)
+#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8)
 STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input)
 {
 	return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL);
@@ -1239,16 +1317,16 @@ STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wch
 static FILE *stbi__fopen(char const *filename, char const *mode)
 {
    FILE *f;
-#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8)
+#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8)
    wchar_t wMode[64];
    wchar_t wFilename[1024];
-	if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)))
+	if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename)))
       return 0;
 
-	if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)))
+	if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode)))
       return 0;
 
-#if _MSC_VER >= 1400
+#if defined(_MSC_VER) && _MSC_VER >= 1400
 	if (0 != _wfopen_s(&f, wFilename, wMode))
 		f = 0;
 #else
@@ -1499,6 +1577,7 @@ enum
 static void stbi__refill_buffer(stbi__context *s)
 {
    int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen);
+   s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original);
    if (n == 0) {
       // at end of file, treat same as if from memory, but need to handle case
       // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file
@@ -1544,6 +1623,7 @@ stbi_inline static int stbi__at_eof(stbi__context *s)
 #else
 static void stbi__skip(stbi__context *s, int n)
 {
+   if (n == 0) return;  // already there!
    if (n < 0) {
       s->img_buffer = s->img_buffer_end;
       return;
@@ -1622,7 +1702,8 @@ static int stbi__get16le(stbi__context *s)
 static stbi__uint32 stbi__get32le(stbi__context *s)
 {
    stbi__uint32 z = stbi__get16le(s);
-   return z + (stbi__get16le(s) << 16);
+   z += (stbi__uint32)stbi__get16le(s) << 16;
+   return z;
 }
 #endif
 
@@ -1686,7 +1767,7 @@ static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int r
          STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]);                   } break;
          STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break;
          STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];                    } break;
-         default: STBI_ASSERT(0);
+         default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion");
       }
       #undef STBI__CASE
    }
@@ -1743,7 +1824,7 @@ static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int r
          STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]);                   } break;
          STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break;
          STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];                       } break;
-         default: STBI_ASSERT(0);
+         default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion");
       }
       #undef STBI__CASE
    }
@@ -2050,13 +2131,12 @@ stbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n)
    int sgn;
    if (j->code_bits < n) stbi__grow_buffer_unsafe(j);
 
-   sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB
+   sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative)
    k = stbi_lrot(j->code_buffer, n);
-   STBI_ASSERT(n >= 0 && n < (int) (sizeof(stbi__bmask)/sizeof(*stbi__bmask)));
    j->code_buffer = k & ~stbi__bmask[n];
    k &= stbi__bmask[n];
    j->code_bits -= n;
-   return k + (stbi__jbias[n] & ~sgn);
+   return k + (stbi__jbias[n] & (sgn - 1));
 }
 
 // get some unsigned bits
@@ -2106,7 +2186,7 @@ static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman
 
    if (j->code_bits < 16) stbi__grow_buffer_unsafe(j);
    t = stbi__jpeg_huff_decode(j, hdc);
-   if (t < 0) return stbi__err("bad huffman code","Corrupt JPEG");
+   if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG");
 
    // 0 all the ac values now so we can do it 32-bits at a time
    memset(data,0,64*sizeof(data[0]));
@@ -2163,11 +2243,12 @@ static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__
       // first scan for DC coefficient, must be first
       memset(data,0,64*sizeof(data[0])); // 0 all the ac values now
       t = stbi__jpeg_huff_decode(j, hdc);
+      if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG");
       diff = t ? stbi__extend_receive(j, t) : 0;
 
       dc = j->img_comp[b].dc_pred + diff;
       j->img_comp[b].dc_pred = dc;
-      data[0] = (short) (dc << j->succ_low);
+      data[0] = (short) (dc * (1 << j->succ_low));
    } else {
       // refinement scan for DC coefficient
       if (stbi__jpeg_get_bit(j))
@@ -2204,7 +2285,7 @@ static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__
             j->code_buffer <<= s;
             j->code_bits -= s;
             zig = stbi__jpeg_dezigzag[k++];
-            data[zig] = (short) ((r >> 8) << shift);
+            data[zig] = (short) ((r >> 8) * (1 << shift));
          } else {
             int rs = stbi__jpeg_huff_decode(j, hac);
             if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG");
@@ -2222,7 +2303,7 @@ static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__
             } else {
                k += r;
                zig = stbi__jpeg_dezigzag[k++];
-               data[zig] = (short) (stbi__extend_receive(j,s) << shift);
+               data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift));
             }
          }
       } while (k <= j->spec_end);
@@ -3153,6 +3234,8 @@ static int stbi__process_frame_header(stbi__jpeg *z, int scan)
    p  = stbi__get8(s);            if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline
    s->img_y = stbi__get16be(s);   if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG
    s->img_x = stbi__get16be(s);   if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires
+   if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
+   if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
    c = stbi__get8(s);
    if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG");
    s->img_n = c;
@@ -3184,6 +3267,13 @@ static int stbi__process_frame_header(stbi__jpeg *z, int scan)
       if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v;
    }
 
+   // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios
+   // and I've never seen a non-corrupted JPEG file actually use them
+   for (i=0; i < s->img_n; ++i) {
+      if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG");
+      if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG");
+   }
+
    // compute interleaved mcu info
    z->img_h_max = h_max;
    z->img_v_max = v_max;
@@ -3739,6 +3829,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
    else
       decode_n = z->s->img_n;
 
+   // nothing to do if no components requested; check this now to avoid
+   // accessing uninitialized coutput[0] later
+   if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; }
+
    // resample and color-convert
    {
       int k;
@@ -3881,6 +3975,7 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re
 {
    unsigned char* result;
    stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg));
+   if (!j) return stbi__errpuc("outofmem", "Out of memory");
    STBI_NOTUSED(ri);
    j->s = s;
    stbi__setup_jpeg(j);
@@ -3893,6 +3988,7 @@ static int stbi__jpeg_test(stbi__context *s)
 {
    int r;
    stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg));
+   if (!j) return stbi__err("outofmem", "Out of memory");
    j->s = s;
    stbi__setup_jpeg(j);
    r = stbi__decode_jpeg_header(j, STBI__SCAN_type);
@@ -3917,6 +4013,7 @@ static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp)
 {
    int result;
    stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg)));
+   if (!j) return stbi__err("outofmem", "Out of memory");
    j->s = s;
    result = stbi__jpeg_info_raw(j, x, y, comp);
    STBI_FREE(j);
@@ -3936,6 +4033,7 @@ static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp)
 // fast-way is faster to check than jpeg huffman, but slow way is slower
 #define STBI__ZFAST_BITS  9 // accelerate all cases in default tables
 #define STBI__ZFAST_MASK  ((1 << STBI__ZFAST_BITS) - 1)
+#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet
 
 // zlib-style huffman encoding
 // (jpegs packs from left, zlib from right, so can't share code)
@@ -3945,8 +4043,8 @@ typedef struct
    stbi__uint16 firstcode[16];
    int maxcode[17];
    stbi__uint16 firstsymbol[16];
-   stbi_uc  size[288];
-   stbi__uint16 value[288];
+   stbi_uc  size[STBI__ZNSYMS];
+   stbi__uint16 value[STBI__ZNSYMS];
 } stbi__zhuffman;
 
 stbi_inline static int stbi__bitreverse16(int n)
@@ -4033,16 +4131,23 @@ typedef struct
    stbi__zhuffman z_length, z_distance;
 } stbi__zbuf;
 
+stbi_inline static int stbi__zeof(stbi__zbuf *z)
+{
+   return (z->zbuffer >= z->zbuffer_end);
+}
+
 stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z)
 {
-   if (z->zbuffer >= z->zbuffer_end) return 0;
-   return *z->zbuffer++;
+   return stbi__zeof(z) ? 0 : *z->zbuffer++;
 }
 
 static void stbi__fill_bits(stbi__zbuf *z)
 {
    do {
-      STBI_ASSERT(z->code_buffer < (1U << z->num_bits));
+      if (z->code_buffer >= (1U << z->num_bits)) {
+        z->zbuffer = z->zbuffer_end;  /* treat this as EOF so we fail. */
+        return;
+      }
       z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits;
       z->num_bits += 8;
    } while (z->num_bits <= 24);
@@ -4067,10 +4172,11 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z)
    for (s=STBI__ZFAST_BITS+1; ; ++s)
       if (k < z->maxcode[s])
          break;
-   if (s == 16) return -1; // invalid code!
+   if (s >= 16) return -1; // invalid code!
    // code size is s, so:
    b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s];
-   STBI_ASSERT(z->size[b] == s);
+   if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere!
+   if (z->size[b] != s) return -1;  // was originally an assert, but report failure instead.
    a->code_buffer >>= s;
    a->num_bits -= s;
    return z->value[b];
@@ -4079,7 +4185,12 @@ static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z)
 stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)
 {
    int b,s;
-   if (a->num_bits < 16) stbi__fill_bits(a);
+   if (a->num_bits < 16) {
+      if (stbi__zeof(a)) {
+         return -1;   /* report error for unexpected end of data. */
+      }
+      stbi__fill_bits(a);
+   }
    b = z->fast[a->code_buffer & STBI__ZFAST_MASK];
    if (b) {
       s = b >> 9;
@@ -4093,13 +4204,16 @@ stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)
 static int stbi__zexpand(stbi__zbuf *z, char *zout, int n)  // need to make room for n bytes
 {
    char *q;
-   int cur, limit, old_limit;
+   unsigned int cur, limit, old_limit;
    z->zout = zout;
    if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG");
-   cur   = (int) (z->zout     - z->zout_start);
-   limit = old_limit = (int) (z->zout_end - z->zout_start);
-   while (cur + n > limit)
+   cur   = (unsigned int) (z->zout - z->zout_start);
+   limit = old_limit = (unsigned) (z->zout_end - z->zout_start);
+   if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory");
+   while (cur + n > limit) {
+      if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory");
       limit *= 2;
+   }
    q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit);
    STBI_NOTUSED(old_limit);
    if (q == NULL) return stbi__err("outofmem", "Out of memory");
@@ -4197,11 +4311,12 @@ static int stbi__compute_huffman_codes(stbi__zbuf *a)
             c = stbi__zreceive(a,2)+3;
             if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG");
             fill = lencodes[n-1];
-         } else if (c == 17)
+         } else if (c == 17) {
             c = stbi__zreceive(a,3)+3;
-         else {
-            STBI_ASSERT(c == 18);
+         } else if (c == 18) {
             c = stbi__zreceive(a,7)+11;
+         } else {
+            return stbi__err("bad codelengths", "Corrupt PNG");
          }
          if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG");
          memset(lencodes+n, fill, c);
@@ -4227,7 +4342,7 @@ static int stbi__parse_uncompressed_block(stbi__zbuf *a)
       a->code_buffer >>= 8;
       a->num_bits -= 8;
    }
-   STBI_ASSERT(a->num_bits == 0);
+   if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG");
    // now fill header the normal way
    while (k < 4)
       header[k++] = stbi__zget8(a);
@@ -4249,6 +4364,7 @@ static int stbi__parse_zlib_header(stbi__zbuf *a)
    int cm    = cmf & 15;
    /* int cinfo = cmf >> 4; */
    int flg   = stbi__zget8(a);
+   if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec
    if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec
    if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png
    if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png
@@ -4256,7 +4372,7 @@ static int stbi__parse_zlib_header(stbi__zbuf *a)
    return 1;
 }
 
-static const stbi_uc stbi__zdefault_length[288] =
+static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] =
 {
    8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
    8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
@@ -4302,7 +4418,7 @@ static int stbi__parse_zlib(stbi__zbuf *a, int parse_header)
       } else {
          if (type == 1) {
             // use fixed code lengths
-            if (!stbi__zbuild_huffman(&a->z_length  , stbi__zdefault_length  , 288)) return 0;
+            if (!stbi__zbuild_huffman(&a->z_length  , stbi__zdefault_length  , STBI__ZNSYMS)) return 0;
             if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance,  32)) return 0;
          } else {
             if (!stbi__compute_huffman_codes(a)) return 0;
@@ -4510,7 +4626,7 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r
          return stbi__err("invalid filter","Corrupt PNG");
 
       if (depth < 8) {
-         STBI_ASSERT(img_width_bytes <= x);
+         if (img_width_bytes > x) return stbi__err("invalid width","Corrupt PNG");
          cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place
          filter_bytes = 1;
          width = img_width_bytes;
@@ -4698,6 +4814,7 @@ static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint3
 
    // de-interlacing
    final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0);
+   if (!final) return stbi__err("outofmem", "Out of memory");
    for (p=0; p < 7; ++p) {
       int xorig[] = { 0,4,0,2,0,1,0 };
       int yorig[] = { 0,0,4,0,2,0,1 };
@@ -4818,19 +4935,46 @@ static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int
    return 1;
 }
 
-static int stbi__unpremultiply_on_load = 0;
-static int stbi__de_iphone_flag = 0;
+static int stbi__unpremultiply_on_load_global = 0;
+static int stbi__de_iphone_flag_global = 0;
 
 STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply)
 {
-   stbi__unpremultiply_on_load = flag_true_if_should_unpremultiply;
+   stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply;
 }
 
 STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert)
 {
-   stbi__de_iphone_flag = flag_true_if_should_convert;
+   stbi__de_iphone_flag_global = flag_true_if_should_convert;
 }
 
+#ifndef STBI_THREAD_LOCAL
+#define stbi__unpremultiply_on_load  stbi__unpremultiply_on_load_global
+#define stbi__de_iphone_flag  stbi__de_iphone_flag_global
+#else
+static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set;
+static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set;
+
+STBIDEF void stbi__unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply)
+{
+   stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply;
+   stbi__unpremultiply_on_load_set = 1;
+}
+
+STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert)
+{
+   stbi__de_iphone_flag_local = flag_true_if_should_convert;
+   stbi__de_iphone_flag_set = 1;
+}
+
+#define stbi__unpremultiply_on_load  (stbi__unpremultiply_on_load_set           \
+                                       ? stbi__unpremultiply_on_load_local      \
+                                       : stbi__unpremultiply_on_load_global)
+#define stbi__de_iphone_flag  (stbi__de_iphone_flag_set                         \
+                                ? stbi__de_iphone_flag_local                    \
+                                : stbi__de_iphone_flag_global)
+#endif // STBI_THREAD_LOCAL
+
 static void stbi__de_iphone(stbi__png *z)
 {
    stbi__context *s = z->s;
@@ -4905,8 +5049,10 @@ static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp)
             if (!first) return stbi__err("multiple IHDR","Corrupt PNG");
             first = 0;
             if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG");
-            s->img_x = stbi__get32be(s); if (s->img_x > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)");
-            s->img_y = stbi__get32be(s); if (s->img_y > (1 << 24)) return stbi__err("too large","Very large image (corrupt?)");
+            s->img_x = stbi__get32be(s);
+            s->img_y = stbi__get32be(s);
+            if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
+            if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
             z->depth = stbi__get8(s);  if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16)  return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only");
             color = stbi__get8(s);  if (color > 6)         return stbi__err("bad ctype","Corrupt PNG");
             if (color == 3 && z->depth == 16)                  return stbi__err("bad ctype","Corrupt PNG");
@@ -5055,10 +5201,12 @@ static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, st
    void *result=NULL;
    if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error");
    if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) {
-      if (p->depth < 8)
+      if (p->depth <= 8)
          ri->bits_per_channel = 8;
+      else if (p->depth == 16)
+         ri->bits_per_channel = 16;
       else
-         ri->bits_per_channel = p->depth;
+         return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth");
       result = p->out;
       p->out = NULL;
       if (req_comp && req_comp != p->s->img_out_n) {
@@ -5207,6 +5355,32 @@ typedef struct
    int extra_read;
 } stbi__bmp_data;
 
+static int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress)
+{
+   // BI_BITFIELDS specifies masks explicitly, don't override
+   if (compress == 3)
+      return 1;
+
+   if (compress == 0) {
+      if (info->bpp == 16) {
+         info->mr = 31u << 10;
+         info->mg = 31u <<  5;
+         info->mb = 31u <<  0;
+      } else if (info->bpp == 32) {
+         info->mr = 0xffu << 16;
+         info->mg = 0xffu <<  8;
+         info->mb = 0xffu <<  0;
+         info->ma = 0xffu << 24;
+         info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0
+      } else {
+         // otherwise, use defaults, which is all-0
+         info->mr = info->mg = info->mb = info->ma = 0;
+      }
+      return 1;
+   }
+   return 0; // error
+}
+
 static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
 {
    int hsz;
@@ -5219,6 +5393,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
    info->mr = info->mg = info->mb = info->ma = 0;
    info->extra_read = 14;
 
+   if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP");
+
    if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown");
    if (hsz == 12) {
       s->img_x = stbi__get16le(s);
@@ -5232,6 +5408,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
    if (hsz != 12) {
       int compress = stbi__get32le(s);
       if (compress == 1 || compress == 2) return stbi__errpuc("BMP RLE", "BMP type not supported: RLE");
+      if (compress >= 4) return stbi__errpuc("BMP JPEG/PNG", "BMP type not supported: unsupported compression"); // this includes PNG/JPEG modes
+      if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc("bad BMP", "bad BMP"); // bitfields requires 16 or 32 bits/pixel
       stbi__get32le(s); // discard sizeof
       stbi__get32le(s); // discard hres
       stbi__get32le(s); // discard vres
@@ -5246,17 +5424,7 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
          }
          if (info->bpp == 16 || info->bpp == 32) {
             if (compress == 0) {
-               if (info->bpp == 32) {
-                  info->mr = 0xffu << 16;
-                  info->mg = 0xffu <<  8;
-                  info->mb = 0xffu <<  0;
-                  info->ma = 0xffu << 24;
-                  info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0
-               } else {
-                  info->mr = 31u << 10;
-                  info->mg = 31u <<  5;
-                  info->mb = 31u <<  0;
-               }
+               stbi__bmp_set_mask_defaults(info, compress);
             } else if (compress == 3) {
                info->mr = stbi__get32le(s);
                info->mg = stbi__get32le(s);
@@ -5271,6 +5439,7 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
                return stbi__errpuc("bad BMP", "bad BMP");
          }
       } else {
+         // V4/V5 header
          int i;
          if (hsz != 108 && hsz != 124)
             return stbi__errpuc("bad BMP", "bad BMP");
@@ -5278,6 +5447,8 @@ static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)
          info->mg = stbi__get32le(s);
          info->mb = stbi__get32le(s);
          info->ma = stbi__get32le(s);
+         if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs
+            stbi__bmp_set_mask_defaults(info, compress);
          stbi__get32le(s); // discard color space
          for (i=0; i < 12; ++i)
             stbi__get32le(s); // discard color space parameters
@@ -5310,6 +5481,9 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req
    flip_vertically = ((int) s->img_y) > 0;
    s->img_y = abs((int) s->img_y);
 
+   if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+   if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+
    mr = info.mr;
    mg = info.mg;
    mb = info.mb;
@@ -5324,7 +5498,9 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req
          psize = (info.offset - info.extra_read - info.hsz) >> 2;
    }
    if (psize == 0) {
-      STBI_ASSERT(info.offset == (s->img_buffer - s->buffer_start));
+      if (info.offset != s->callback_already_read + (s->img_buffer - s->img_buffer_original)) {
+        return stbi__errpuc("bad offset", "Corrupt BMP");
+      }
    }
 
    if (info.bpp == 24 && ma == 0xff000000)
@@ -5419,6 +5595,7 @@ static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req
          gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg);
          bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb);
          ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma);
+         if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); }
       }
       for (j=0; j < (int) s->img_y; ++j) {
          if (easy) {
@@ -5643,6 +5820,9 @@ static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req
    STBI_NOTUSED(tga_x_origin); // @TODO
    STBI_NOTUSED(tga_y_origin); // @TODO
 
+   if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+   if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+
    //   do a tiny bit of precessing
    if ( tga_image_type >= 8 )
    {
@@ -5682,6 +5862,11 @@ static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req
       //   do I need to load a palette?
       if ( tga_indexed)
       {
+         if (tga_palette_len == 0) {  /* you have to have at least one entry! */
+            STBI_FREE(tga_data);
+            return stbi__errpuc("bad palette", "Corrupt TGA");
+         }
+
          //   any data to skip? (offset usually = 0)
          stbi__skip(s, tga_palette_start );
          //   load the palette
@@ -5890,6 +6075,9 @@ static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req
    h = stbi__get32be(s);
    w = stbi__get32be(s);
 
+   if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+   if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+
    // Make sure the depth is 8 bits.
    bitdepth = stbi__get16be(s);
    if (bitdepth != 8 && bitdepth != 16)
@@ -6244,6 +6432,10 @@ static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_c
 
    x = stbi__get16be(s);
    y = stbi__get16be(s);
+
+   if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+   if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+
    if (stbi__at_eof(s))  return stbi__errpuc("bad file","file too short (pic header)");
    if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode");
 
@@ -6253,6 +6445,7 @@ static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_c
 
    // intermediate buffer is RGBA
    result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0);
+   if (!result) return stbi__errpuc("outofmem", "Out of memory");
    memset(result, 0xff, x*y*4);
 
    if (!stbi__pic_load_core(s,x,y,comp, result)) {
@@ -6352,6 +6545,9 @@ static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_in
    g->ratio = stbi__get8(s);
    g->transparent = -1;
 
+   if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
+   if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)");
+
    if (comp != 0) *comp = 4;  // can't actually tell whether it's 3 or 4 until we parse the comments
 
    if (is_info) return 1;
@@ -6365,6 +6561,7 @@ static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_in
 static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp)
 {
    stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif));
+   if (!g) return stbi__err("outofmem", "Out of memory");
    if (!stbi__gif_header(s, g, comp, 1)) {
       STBI_FREE(g);
       stbi__rewind( s );
@@ -6529,7 +6726,7 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i
       memset(g->history, 0x00, pcount);        // pixels that were affected previous frame
       first_frame = 1;
    } else {
-      // second frame - how do we dispoase of the previous one?
+      // second frame - how do we dispose of the previous one?
       dispose = (g->eflags & 0x1C) >> 2;
       pcount = g->w * g->h;
 
@@ -6674,6 +6871,17 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i
    }
 }
 
+static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays)
+{
+   STBI_FREE(g->out);
+   STBI_FREE(g->history);
+   STBI_FREE(g->background);
+
+   if (out) STBI_FREE(out);
+   if (delays && *delays) STBI_FREE(*delays);
+   return stbi__errpuc("outofmem", "Out of memory");
+}
+
 static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp)
 {
    if (stbi__gif_test(s)) {
@@ -6683,6 +6891,12 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
       stbi_uc *two_back = 0;
       stbi__gif g;
       int stride;
+      int out_size = 0;
+      int delays_size = 0;
+
+      STBI_NOTUSED(out_size);
+      STBI_NOTUSED(delays_size);
+
       memset(&g, 0, sizeof(g));
       if (delays) {
          *delays = 0;
@@ -6699,22 +6913,31 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
             stride = g.w * g.h * 4;
 
             if (out) {
-               void *tmp = (stbi_uc*) STBI_REALLOC( out, layers * stride );
-               if (NULL == tmp) {
-                  STBI_FREE(g.out);
-                  STBI_FREE(g.history);
-                  STBI_FREE(g.background);
-                  return stbi__errpuc("outofmem", "Out of memory");
+               void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride );
+               if (!tmp)
+                  return stbi__load_gif_main_outofmem(&g, out, delays);
+               else {
+                   out = (stbi_uc*) tmp;
+                   out_size = layers * stride;
                }
-               else
-                  out = (stbi_uc*) tmp;
+
                if (delays) {
-                  *delays = (int*) STBI_REALLOC( *delays, sizeof(int) * layers );
+                  int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers );
+                  if (!new_delays)
+                     return stbi__load_gif_main_outofmem(&g, out, delays);
+                  *delays = new_delays;
+                  delays_size = layers * sizeof(int);
                }
             } else {
                out = (stbi_uc*)stbi__malloc( layers * stride );
+               if (!out)
+                  return stbi__load_gif_main_outofmem(&g, out, delays);
+               out_size = layers * stride;
                if (delays) {
                   *delays = (int*) stbi__malloc( layers * sizeof(int) );
+                  if (!*delays)
+                     return stbi__load_gif_main_outofmem(&g, out, delays);
+                  delays_size = layers * sizeof(int);
                }
             }
             memcpy( out + ((layers - 1) * stride), u, stride );
@@ -6893,6 +7116,9 @@ static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int re
    token += 3;
    width = (int) strtol(token, NULL, 10);
 
+   if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)");
+   if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)");
+
    *x = width;
    *y = height;
 
@@ -7035,9 +7261,10 @@ static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp)
 
    info.all_a = 255;
    p = stbi__bmp_parse_header(s, &info);
-   stbi__rewind( s );
-   if (p == NULL)
+   if (p == NULL) {
+      stbi__rewind( s );
       return 0;
+   }
    if (x) *x = s->img_x;
    if (y) *y = s->img_y;
    if (comp) {
@@ -7103,8 +7330,8 @@ static int stbi__psd_is16(stbi__context *s)
        stbi__rewind( s );
        return 0;
    }
-   (void) stbi__get32be(s);
-   (void) stbi__get32be(s);
+   STBI_NOTUSED(stbi__get32be(s));
+   STBI_NOTUSED(stbi__get32be(s));
    depth = stbi__get16be(s);
    if (depth != 16) {
        stbi__rewind( s );
@@ -7183,7 +7410,6 @@ static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp)
 // Known limitations:
 //    Does not support comments in the header section
 //    Does not support ASCII image data (formats P2 and P3)
-//    Does not support 16-bit-per-channel
 
 #ifndef STBI_NO_PNM
 
@@ -7204,19 +7430,23 @@ static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req
    stbi_uc *out;
    STBI_NOTUSED(ri);
 
-   if (!stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n))
+   ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n);
+   if (ri->bits_per_channel == 0)
       return 0;
 
+   if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+   if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)");
+
    *x = s->img_x;
    *y = s->img_y;
    if (comp) *comp = s->img_n;
 
-   if (!stbi__mad3sizes_valid(s->img_n, s->img_x, s->img_y, 0))
+   if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0))
       return stbi__errpuc("too large", "PNM too large");
 
-   out = (stbi_uc *) stbi__malloc_mad3(s->img_n, s->img_x, s->img_y, 0);
+   out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0);
    if (!out) return stbi__errpuc("outofmem", "Out of memory");
-   stbi__getn(s, out, s->img_n * s->img_x * s->img_y);
+   stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8));
 
    if (req_comp && req_comp != s->img_n) {
       out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y);
@@ -7292,11 +7522,19 @@ static int      stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp)
    stbi__pnm_skip_whitespace(s, &c);
 
    maxv = stbi__pnm_getinteger(s, &c);  // read max value
-
-   if (maxv > 255)
-      return stbi__err("max value > 255", "PPM image not 8-bit");
+   if (maxv > 65535)
+      return stbi__err("max value > 65535", "PPM image supports only 8-bit and 16-bit images");
+   else if (maxv > 255)
+      return 16;
    else
-      return 1;
+      return 8;
+}
+
+static int stbi__pnm_is16(stbi__context *s)
+{
+   if (stbi__pnm_info(s, NULL, NULL, NULL) == 16)
+	   return 1;
+   return 0;
 }
 #endif
 
@@ -7352,6 +7590,9 @@ static int stbi__is_16_main(stbi__context *s)
    if (stbi__psd_is16(s))  return 1;
    #endif
 
+   #ifndef STBI_NO_PNM
+   if (stbi__pnm_is16(s))  return 1;
+   #endif
    return 0;
 }
 
diff --git a/ui/thirdparty/stb_image/stb_image_impl.c b/ui/thirdparty/stb_image/stb_image_impl.c
new file mode 100644
index 0000000000..8ddfd1f546
--- /dev/null
+++ b/ui/thirdparty/stb_image/stb_image_impl.c
@@ -0,0 +1,2 @@
+#define STB_IMAGE_IMPLEMENTATION
+#include "stb_image.h"
diff --git a/ui/xemu-custom-widgets.c b/ui/xemu-custom-widgets.c
deleted file mode 100644
index f31b4c5d12..0000000000
--- a/ui/xemu-custom-widgets.c
+++ /dev/null
@@ -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);
-}
diff --git a/ui/xemu-custom-widgets.h b/ui/xemu-custom-widgets.h
deleted file mode 100644
index 776113423f..0000000000
--- a/ui/xemu-custom-widgets.h
+++ /dev/null
@@ -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
diff --git a/ui/xemu-hud.cc b/ui/xemu-hud.cc
deleted file mode 100644
index 50eea79712..0000000000
--- a/ui/xemu-hud.cc
+++ /dev/null
@@ -1,2412 +0,0 @@
-/*
- * xemu User Interface
- *
- * 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 <deque>
-#include <vector>
-#include <string>
-#include <memory>
-
-#include "xemu-hud.h"
-#include "xemu-input.h"
-#include "xemu-notifications.h"
-#include "xemu-settings.h"
-#include "xemu-shaders.h"
-#include "xemu-custom-widgets.h"
-#include "xemu-monitor.h"
-#include "xemu-version.h"
-#include "xemu-net.h"
-#include "xemu-os-utils.h"
-#include "xemu-xbe.h"
-#include "xemu-reporting.h"
-
-#if defined(_WIN32)
-#include "xemu-update.h"
-#endif
-
-#include "data/roboto_medium.ttf.h"
-
-#include "imgui/imgui.h"
-#include "imgui/backends/imgui_impl_sdl.h"
-#include "imgui/backends/imgui_impl_opengl3.h"
-#include "implot/implot.h"
-
-extern "C" {
-#include "noc_file_dialog.h"
-
-// Include necessary QEMU headers
-#include "qemu/osdep.h"
-#include "qemu-common.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"
-#include "net/pcap.h"
-
-#undef typename
-#undef atomic_fetch_add
-#undef atomic_fetch_and
-#undef atomic_fetch_xor
-#undef atomic_fetch_or
-#undef atomic_fetch_sub
-}
-
-ImFont *g_fixed_width_font;
-float g_main_menu_height;
-float g_ui_scale = 1.0;
-bool g_trigger_style_update = true;
-
-class NotificationManager
-{
-private:
-    const int kNotificationDuration = 4000;
-    std::deque<const char *> notification_queue;
-    bool active;
-    uint32_t notification_end_ts;
-    const char *msg;
-
-public:
-    NotificationManager()
-    {
-        active = false;
-    }
-
-    ~NotificationManager()
-    {
-
-    }
-
-    void QueueNotification(const char *msg)
-    {
-        notification_queue.push_back(strdup(msg));
-    }
-
-    void Draw()
-    {
-        uint32_t now = SDL_GetTicks();
-
-        if (active) {
-            // Currently displaying a notification
-            float t = (notification_end_ts - now)/(float)kNotificationDuration;
-            if (t > 1.0) {
-                // Notification delivered, free it
-                free((void*)msg);
-                active = false;
-            } else {
-                // Notification should be displayed
-                DrawNotification(t, msg);
-            }
-        } else {
-            // Check to see if a notification is pending
-            if (notification_queue.size() > 0) {
-                msg = notification_queue[0];
-                active = true;
-                notification_end_ts = now + kNotificationDuration;
-                notification_queue.pop_front();
-            }
-        }
-    }
-
-private:
-    void 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();
-    }
-};
-
-static 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();
-    }
-}
-
-static void Hyperlink(const char *text, const char *url)
-{
-    // FIXME: Color text when hovered
-    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_ui_scale;
-    min.y = max.y;
-    max.x -= 1 * g_ui_scale;
-    ImGui::GetWindowDrawList()->AddLine(min, max, col, 1.0 * g_ui_scale);
-
-    if (ImGui::IsItemClicked()) {
-        xemu_open_web_browser(url);
-    }
-}
-
-static 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;
-}
-
-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()
-    {
-        is_open = false;
-        memset(InputBuf, 0, sizeof(InputBuf));
-        HistoryPos = -1;
-        AutoScroll = true;
-        ScrollToBottom = false;
-    }
-    ~MonitorWindow()
-    {
-    }
-
-    // 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; }
-
-    void Draw()
-    {
-        if (!is_open) return;
-        int style_pop_cnt = PushWindowTransparencySettings(true);
-        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_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_fixed_width_font);
-            if (ImGui::InputText("", 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 toggle_open(void)
-    {
-        is_open = !is_open;
-    }
-
-private:
-    void 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;
-    }
-
-    static int 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 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;
-    }
-};
-
-class InputWindow
-{
-public:
-    bool is_open;
-
-    InputWindow()
-    {
-        is_open = false;
-    }
-
-    ~InputWindow()
-    {
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        ImGui::SetNextWindowContentSize(ImVec2(500.0f*g_ui_scale, 0.0f));
-        // Remove window X padding for this window to easily center stuff
-        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0,ImGui::GetStyle().WindowPadding.y));
-        if (!ImGui::Begin("Input", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize))
-        {
-            ImGui::End();
-            ImGui::PopStyleVar();
-            return;
-        }
-
-        static int active = 0;
-
-        // Output dimensions of texture
-        float t_w = 512, t_h = 512;
-        // Dimensions of (port+label)s
-        float b_x = 0, b_x_stride = 100, b_y = 400;
-        float b_w = 68, b_h = 81;
-        // Dimensions of controller (rendered at origin)
-        float controller_width  = 477.0f;
-        float controller_height = 395.0f;
-
-        // Setup rendering to fbo for controller and port images
-        ImTextureID id = (ImTextureID)(intptr_t)render_to_fbo(controller_fbo);
-
-        //
-        // Render buttons with icons of the Xbox style port sockets with
-        // circular numbers above them. These buttons can be activated to
-        // configure the associated port, like a tabbed interface.
-        //
-        ImVec4 color_active(0.50, 0.86, 0.54, 0.12);
-        ImVec4 color_inactive(0, 0, 0, 0);
-
-        // Begin a 4-column layout to render the ports
-        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,12));
-        ImGui::Columns(4, "mixed", false);
-
-        const int port_padding = 8;
-        for (int i = 0; i < 4; i++) {
-            bool is_currently_selected = (i == active);
-            bool port_is_bound = (xemu_input_get_bound(i) != NULL);
-
-            // Set an X offset to center the image button within the column
-            ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-b_w*g_ui_scale-2*port_padding*g_ui_scale)/2));
-
-            // We are using the same texture for all buttons, but ImageButton
-            // uses the texture as a unique ID. Push a new ID now to resolve
-            // the conflict.
-            ImGui::PushID(i);
-            float x = b_x+i*b_x_stride;
-            ImGui::PushStyleColor(ImGuiCol_Button, is_currently_selected ? color_active : color_inactive);
-            bool activated = ImGui::ImageButton(id,
-                ImVec2(b_w*g_ui_scale,b_h*g_ui_scale),
-                ImVec2(x/t_w, (b_y+b_h)/t_h),
-                ImVec2((x+b_w)/t_w, b_y/t_h),
-                port_padding);
-            ImGui::PopStyleColor();
-
-            if (activated) {
-                active = i;
-            }
-
-            uint32_t port_color = 0xafafafff;
-            bool is_hovered = ImGui::IsItemHovered();
-            if (is_currently_selected || port_is_bound) {
-                port_color = 0x81dc8a00;
-            } else if (is_hovered) {
-                port_color = 0x000000ff;
-            }
-
-            render_controller_port(x, b_y, i, port_color);
-
-            ImGui::PopID();
-            ImGui::NextColumn();
-        }
-        ImGui::PopStyleVar(); // ItemSpacing
-        ImGui::Columns(1);
-
-        //
-        // Render input device combo
-        //
-
-        // Center the combo above the controller with the same width
-        ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-controller_width*g_ui_scale)/2.0));
-
-        // Note: SetNextItemWidth applies only to the combo element, but not the
-        // associated label which follows, so scale back a bit to make space for
-        // the label.
-        ImGui::SetNextItemWidth(controller_width*0.75*g_ui_scale);
-
-        // List available input devices
-        const char *not_connected = "Not Connected";
-        ControllerState *bound_state = xemu_input_get_bound(active);
-
-        // Get current controller name
-        const char *name;
-        if (bound_state == NULL) {
-            name = not_connected;
-        } else {
-            name = bound_state->name;
-        }
-
-        if (ImGui::BeginCombo("Input Devices", name))
-        {
-            // Handle "Not connected"
-            bool is_selected = bound_state == NULL;
-            if (ImGui::Selectable(not_connected, is_selected)) {
-                xemu_input_bind(active, NULL, 1);
-                bound_state = NULL;
-            }
-            if (is_selected) {
-                ImGui::SetItemDefaultFocus();
-            }
-
-            // Handle all available input devices
-            ControllerState *iter;
-            QTAILQ_FOREACH(iter, &available_controllers, entry) {
-                is_selected = bound_state == iter;
-                ImGui::PushID(iter);
-                const char *selectable_label = iter->name;
-                char buf[128];
-                if (iter->bound >= 0) {
-                    snprintf(buf, sizeof(buf), "%s (Port %d)", iter->name, iter->bound+1);
-                    selectable_label = buf;
-                }
-                if (ImGui::Selectable(selectable_label, is_selected)) {
-                    xemu_input_bind(active, iter, 1);
-                    bound_state = iter;
-                }
-                if (is_selected) {
-                    ImGui::SetItemDefaultFocus();
-                }
-                ImGui::PopID();
-            }
-
-            ImGui::EndCombo();
-        }
-
-        ImGui::Columns(1);
-
-        //
-        // Add a separator between input selection and controller graphic
-        //
-        ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
-        ImGui::Separator();
-        ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
-
-        //
-        // Render controller image
-        //
-        bool device_selected = false;
-
-        if (bound_state) {
-            device_selected = true;
-            render_controller(0, 0, 0x81dc8a00, 0x0f0f0f00, bound_state);
-        } else {
-            static ControllerState state = { 0 };
-            render_controller(0, 0, 0x1f1f1f00, 0x0f0f0f00, &state);
-        }
-
-        // update_sdl_controller_state(&state);
-        // update_sdl_kbd_controller_state(&state);
-        ImVec2 cur = ImGui::GetCursorPos();
-        ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-controller_width*g_ui_scale)/2.0));
-        ImGui::Image(id,
-            ImVec2(controller_width*g_ui_scale, controller_height*g_ui_scale),
-            ImVec2(0, controller_height/t_h),
-            ImVec2(controller_width/t_w, 0));
-
-        if (!device_selected) {
-            // ImGui::SameLine();
-            const char *msg = "Please select an available input device";
-            ImVec2 dim = ImGui::CalcTextSize(msg);
-            ImGui::SetCursorPosX(cur.x + (controller_width*g_ui_scale-dim.x)/2);
-            ImGui::SetCursorPosY(cur.y + (controller_height*g_ui_scale-dim.y)/2);
-            ImGui::Text("%s", msg);
-            ImGui::SameLine();
-        }
-
-        ImGui::End();
-        ImGui::PopStyleVar(); // Window padding
-
-        // Restore original framebuffer target
-        render_to_default_fb();
-    }
-};
-
-static const char *paused_file_open(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;
-}
-
-#define MAX_STRING_LEN 2048 // FIXME: Completely arbitrary and only used here
-                            // to give a buffer to ImGui for each field
-
-class SettingsWindow
-{
-public:
-    bool is_open;
-
-private:
-    bool dirty;
-    bool pending_restart;
-
-    char flashrom_path[MAX_STRING_LEN];
-    char bootrom_path[MAX_STRING_LEN];
-    char hdd_path[MAX_STRING_LEN];
-    char eeprom_path[MAX_STRING_LEN];
-
-public:
-    SettingsWindow()
-    {
-        is_open = false;
-        dirty = false;
-        pending_restart = false;
-
-        flashrom_path[0] = '\0';
-        bootrom_path[0] = '\0';
-        hdd_path[0] = '\0';
-        eeprom_path[0] = '\0';
-    }
-
-    ~SettingsWindow()
-    {
-    }
-
-    void Load()
-    {
-        strncpy(flashrom_path, g_config.sys.files.flashrom_path, sizeof(flashrom_path)-1);
-        strncpy(bootrom_path, g_config.sys.files.bootrom_path, sizeof(bootrom_path)-1);
-        strncpy(hdd_path, g_config.sys.files.hdd_path, sizeof(hdd_path)-1);
-        strncpy(eeprom_path, g_config.sys.files.eeprom_path, sizeof(eeprom_path)-1);
-        dirty = false;
-    }
-
-    void Save()
-    {
-        xemu_settings_set_string(&g_config.sys.files.flashrom_path, flashrom_path);
-        xemu_settings_set_string(&g_config.sys.files.bootrom_path, bootrom_path);
-        xemu_settings_set_string(&g_config.sys.files.hdd_path, hdd_path);
-        xemu_settings_set_string(&g_config.sys.files.eeprom_path, eeprom_path);
-        xemu_queue_notification("Settings saved. Restart to apply updates.");
-        pending_restart = true;
-        g_config.general.show_welcome = false;
-    }
-
-    void FilePicker(const char *name, char *buf, size_t len, const char *filters)
-    {
-        ImGui::PushID(name);
-        if (ImGui::InputText("", buf, len)) {
-            dirty = true;
-        }
-        ImGui::SameLine();
-        if (ImGui::Button("Browse...", ImVec2(100*g_ui_scale, 0))) {
-            const char *selected = paused_file_open(NOC_FILE_DIALOG_OPEN, filters, buf, NULL);
-            if ((selected != NULL) && (strcmp(buf, selected) != 0)) {
-                strncpy(buf, selected, len-1);
-                dirty = true;
-            }
-        }
-        ImGui::PopID();
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_ui_scale, 0.0f));
-        if (!ImGui::Begin("Settings", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize))
-        {
-            ImGui::End();
-            return;
-        }
-
-        if (ImGui::IsWindowAppearing()) {
-            Load();
-        }
-
-        const char *rom_file_filters = ".bin Files\0*.bin\0.rom Files\0*.rom\0All Files\0*.*\0";
-        const char *qcow_file_filters = ".qcow2 Files\0*.qcow2\0All Files\0*.*\0";
-
-        ImGui::Columns(2, "", false);
-        ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.25);
-
-        ImGui::Text("Flash (BIOS) File");
-        ImGui::NextColumn();
-        float picker_width = ImGui::GetColumnWidth()-120*g_ui_scale;
-        ImGui::SetNextItemWidth(picker_width);
-        FilePicker("###Flash", flashrom_path, sizeof(flashrom_path), rom_file_filters);
-        ImGui::NextColumn();
-
-        ImGui::Text("MCPX Boot ROM File");
-        ImGui::NextColumn();
-        ImGui::SetNextItemWidth(picker_width);
-        FilePicker("###BootROM", bootrom_path, sizeof(bootrom_path), rom_file_filters);
-        ImGui::NextColumn();
-
-        ImGui::Text("Hard Disk Image File");
-        ImGui::NextColumn();
-        ImGui::SetNextItemWidth(picker_width);
-        FilePicker("###HDD", hdd_path, sizeof(hdd_path), qcow_file_filters);
-        ImGui::NextColumn();
-
-        ImGui::Text("EEPROM File");
-        ImGui::NextColumn();
-        ImGui::SetNextItemWidth(picker_width);
-        FilePicker("###EEPROM", eeprom_path, sizeof(eeprom_path), rom_file_filters);
-        ImGui::NextColumn();
-
-        ImGui::Text("System Memory");
-        ImGui::NextColumn();
-        ImGui::SetNextItemWidth(ImGui::GetColumnWidth()*0.5);
-        ImGui::Combo("###mem", &g_config.sys.mem_limit, "64 MiB\0" "128 MiB\0");
-        ImGui::NextColumn();
-
-        ImGui::Dummy(ImVec2(0,0));
-        ImGui::NextColumn();
-        ImGui::Checkbox("Skip startup animation", &g_config.general.misc.skip_boot_anim);
-        ImGui::NextColumn();
-
-#if defined(_WIN32)
-        ImGui::Dummy(ImVec2(0,0));
-        ImGui::NextColumn();
-        ImGui::Checkbox("Check for updates on startup", &g_config.general.updates.check);
-        ImGui::NextColumn();
-#endif
-
-        ImGui::Columns(1);
-
-        ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
-        ImGui::Separator();
-        ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
-
-        Hyperlink("Help", "https://xemu.app/docs/getting-started/");
-        ImGui::SameLine();
-
-        const char *msg = NULL;
-        if (dirty) {
-            msg = "Warning: Unsaved changes!";
-        } else if (pending_restart) {
-            msg = "Restart to apply updates";
-        }
-
-        if (msg) {
-            ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2.0);
-            ImGui::Text("%s", msg);
-            ImGui::SameLine();
-        }
-
-        ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)*g_ui_scale);
-        ImGui::SetItemDefaultFocus();
-        if (ImGui::Button("Save", ImVec2(120*g_ui_scale, 0))) {
-            Save();
-            dirty = false;
-            pending_restart = true;
-        }
-
-        ImGui::End();
-    }
-};
-
-static const char *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;
-}
-
-#ifndef _WIN32
-#ifdef CONFIG_CPUID_H
-#include <cpuid.h>
-#endif
-#endif
-
-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;
-}
-
-class AboutWindow
-{
-public:
-    bool is_open;
-
-private:
-    char build_info_text[256];
-    char platform_info_text[350];
-
-public:
-    AboutWindow()
-    {
-        snprintf(build_info_text, sizeof(build_info_text),
-            "Version:      %s\nBranch:       %s\nCommit:       %s\nDate:         %s",
-            xemu_version, xemu_branch, xemu_commit, xemu_date);
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        ImGui::SetNextWindowContentSize(ImVec2(400.0f*g_ui_scale, 0.0f));
-        if (!ImGui::Begin("About", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize))
-        {
-            ImGui::End();
-            return;
-        }
-
-        static uint32_t time_start = 0;
-        if (ImGui::IsWindowAppearing()) { 
-            const char *gl_shader_version = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
-            const char *gl_version = (const char*)glGetString(GL_VERSION);
-            const char *gl_renderer = (const char*)glGetString(GL_RENDERER);
-            const char *gl_vendor = (const char*)glGetString(GL_VENDOR);
-             
-            snprintf(platform_info_text, sizeof(platform_info_text),
-                "CPU:          %s\nOS Platform:  %s\nOS Version:   %s\nManufacturer: %s\n"
-                "GPU Model:    %s\nDriver:       %s\nShader:       %s",
-                 xemu_get_cpu_info(), get_os_platform(), xemu_get_os_info(), gl_vendor,
-                 gl_renderer, gl_version, gl_shader_version);
-            // FIXME: Show BIOS/BootROM hash
-
-            time_start = SDL_GetTicks();
-        }
-        uint32_t now = SDL_GetTicks() - time_start;
-
-        ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50*g_ui_scale);
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256*g_ui_scale)/2);
-
-        ImTextureID id = (ImTextureID)(intptr_t)render_to_fbo(logo_fbo);
-        float t_w = 256.0;
-        float t_h = 256.0;
-        float x_off = 0;
-        ImGui::Image(id,
-            ImVec2((t_w-x_off)*g_ui_scale, t_h*g_ui_scale),
-            ImVec2(x_off/t_w, t_h/t_h),
-            ImVec2(t_w/t_w, 0));
-        if (ImGui::IsItemClicked()) {
-            time_start = SDL_GetTicks();
-        }
-        render_logo(now, 0x42e335ff, 0x42e335ff, 0x00000000);
-        render_to_default_fb();
-        ImGui::SetCursorPosX(10*g_ui_scale);
-
-        ImGui::SetCursorPosY(ImGui::GetCursorPosY()-100*g_ui_scale);
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(xemu_version).x)/2);
-        ImGui::Text("%s", xemu_version);
-
-        ImGui::SetCursorPosX(10*g_ui_scale);
-        ImGui::Dummy(ImVec2(0,20*g_ui_scale));
-
-        const char *msg = "Visit https://xemu.app for more information";
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
-        Hyperlink(msg, "https://xemu.app");
-
-        ImGui::Dummy(ImVec2(0,40*g_ui_scale));
-
-        ImGui::PushFont(g_fixed_width_font);
-        ImGui::InputTextMultiline("##build_info", build_info_text, sizeof(build_info_text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 5), ImGuiInputTextFlags_ReadOnly);
-        ImGui::InputTextMultiline("##platform_info", platform_info_text, sizeof(platform_info_text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 8), ImGuiInputTextFlags_ReadOnly);
-        ImGui::PopFont();
-
-        ImGui::End();
-    }
-};
-
-class NetworkInterface
-{
-public:
-    std::string pcap_name;
-    std::string description;
-    std::string friendlyname;
-
-    NetworkInterface(pcap_if_t *pcap_desc, char *_friendlyname = NULL)
-    {
-        pcap_name = pcap_desc->name;
-        description = pcap_desc->description ?: pcap_desc->name;
-        if (_friendlyname) {
-            char *tmp = g_strdup_printf("%s (%s)", _friendlyname, description.c_str());
-            friendlyname = tmp;
-            g_free((gpointer)tmp);
-        } else {
-            friendlyname = description;
-        }
-    }
-};
-
-class NetworkInterfaceManager
-{
-public:
-    std::vector<std::unique_ptr<NetworkInterface>> ifaces;
-    NetworkInterface *current_iface;
-    bool failed_to_load_lib;
-
-    NetworkInterfaceManager()
-    {
-        current_iface = NULL;
-        failed_to_load_lib = false;
-    }
-
-    void refresh(void)
-    {
-        pcap_if_t *alldevs, *iter;
-        char err[PCAP_ERRBUF_SIZE];
-
-        if (xemu_net_is_enabled()) {
-            return;
-        }
-
-#if defined(_WIN32)
-        if (pcap_load_library()) {
-            failed_to_load_lib = true;
-            return;
-        }
-#endif
-
-        ifaces.clear();
-        current_iface = NULL;
-
-        if (pcap_findalldevs(&alldevs, err)) {
-            return;
-        }
-
-        for (iter=alldevs; iter != NULL; iter=iter->next) {
-#if defined(_WIN32)
-            char *friendlyname = get_windows_interface_friendly_name(iter->name);
-            ifaces.emplace_back(new NetworkInterface(iter, friendlyname));
-            if (friendlyname) {
-                g_free((gpointer)friendlyname);
-            }
-#else
-            ifaces.emplace_back(new NetworkInterface(iter));
-#endif
-            if (!strcmp(g_config.net.pcap.netif, iter->name)) {
-                current_iface = ifaces.back().get();
-            }
-        }
-
-        pcap_freealldevs(alldevs);
-    }
-
-    void select(NetworkInterface &iface)
-    {
-        current_iface = &iface;
-        xemu_settings_set_string(&g_config.net.pcap.netif,
-                                 iface.pcap_name.c_str());
-    }
-
-    bool is_current(NetworkInterface &iface)
-    {
-        return &iface == current_iface;
-    }
-};
-
-
-class NetworkWindow
-{
-public:
-    bool is_open;
-    char remote_addr[64];
-    char local_addr[64];
-    std::unique_ptr<NetworkInterfaceManager> iface_mgr;
-
-    NetworkWindow()
-    {
-        is_open = false;
-    }
-
-    ~NetworkWindow()
-    {
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        ImGui::SetNextWindowContentSize(ImVec2(500.0f*g_ui_scale, 0.0f));
-        if (!ImGui::Begin("Network", &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
-            ImGui::End();
-            return;
-        }
-
-        if (ImGui::IsWindowAppearing()) {
-            strncpy(remote_addr, g_config.net.udp.remote_addr, sizeof(remote_addr)-1);
-            strncpy(local_addr, g_config.net.udp.bind_addr, sizeof(local_addr)-1);
-        }
-
-        ImGuiInputTextFlags flg = 0;
-        bool is_enabled = xemu_net_is_enabled();
-        if (is_enabled) {
-            flg |= ImGuiInputTextFlags_ReadOnly;
-        }
-
-        ImGui::Columns(2, "", false);
-        ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.33);
-
-        ImGui::Text("Attached To");
-        ImGui::SameLine(); HelpMarker("The network backend which the emulated NIC interacts with");
-        ImGui::NextColumn();
-        if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
-        int temp_backend = g_config.net.backend; // Temporary to make backend combo read-only (FIXME: surely there's a nicer way)
-        ImGui::Combo("##backend", is_enabled ? &temp_backend : &g_config.net.backend, "NAT\0UDP Tunnel\0Bridged Adapter\0");
-        if (is_enabled) ImGui::PopStyleVar();
-        ImGui::SameLine();
-        if (g_config.net.backend == CONFIG_NET_BACKEND_NAT) {
-            HelpMarker("User-mode TCP/IP stack with network address translation");
-        } else if (g_config.net.backend == CONFIG_NET_BACKEND_UDP) {
-            HelpMarker("Tunnels link-layer traffic to a remote host via UDP");
-        } else if (g_config.net.backend == CONFIG_NET_BACKEND_PCAP) {
-            HelpMarker("Bridges with a host network interface");
-        }
-        ImGui::NextColumn();
-
-        if (g_config.net.backend == CONFIG_NET_BACKEND_UDP) {
-            ImGui::Text("Remote Host");
-            ImGui::SameLine(); HelpMarker("The remote <IP address>:<Port> to forward packets to (e.g. 1.2.3.4:9368)");
-            ImGui::NextColumn();
-            float w = ImGui::GetColumnWidth()-10*g_ui_scale;
-            ImGui::SetNextItemWidth(w);
-            if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
-            ImGui::InputText("###remote_host", remote_addr, sizeof(remote_addr), flg);
-            if (is_enabled) ImGui::PopStyleVar();
-            ImGui::NextColumn();
-
-            ImGui::Text("Local Host");
-            ImGui::SameLine(); HelpMarker("The local <IP address>:<Port> to receive packets on (e.g. 0.0.0.0:9368)");
-            ImGui::NextColumn();
-            ImGui::SetNextItemWidth(w);
-            if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
-            ImGui::InputText("###local_host", local_addr, sizeof(local_addr), flg);
-            if (is_enabled) ImGui::PopStyleVar();
-            ImGui::NextColumn();
-        } else if (g_config.net.backend == CONFIG_NET_BACKEND_PCAP) {
-            static bool should_refresh = true;
-            if (iface_mgr.get() == nullptr) {
-                iface_mgr.reset(new NetworkInterfaceManager());
-                iface_mgr->refresh();
-            }
-
-            if (iface_mgr->failed_to_load_lib) {
-#if defined(_WIN32)
-                ImGui::Columns(1);
-                ImGui::Dummy(ImVec2(0,20*g_ui_scale));
-                const char *msg = "WinPcap/npcap library could not be loaded.\n"
-                                  "To use this attachment, please install npcap.";
-                ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetColumnWidth() - g_ui_scale*ImGui::CalcTextSize(msg).x)/2);
-                ImGui::Text("%s", msg);
-                ImGui::Dummy(ImVec2(0,10*g_ui_scale));
-                ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120*g_ui_scale)/2);
-                if (ImGui::Button("Install npcap", ImVec2(120*g_ui_scale, 0))) {
-                    xemu_open_web_browser("https://nmap.org/npcap/");
-                }
-                ImGui::Dummy(ImVec2(0,10*g_ui_scale));
-#endif
-            } else {
-                ImGui::Text("Network Interface");
-                ImGui::SameLine(); HelpMarker("Host network interface to bridge with");
-                ImGui::NextColumn();
-
-                float w = ImGui::GetColumnWidth()-10*g_ui_scale;
-                ImGui::SetNextItemWidth(w);
-                const char *selected_display_name = (
-                    iface_mgr->current_iface
-                    ? iface_mgr->current_iface->friendlyname.c_str()
-                    : g_config.net.pcap.netif
-                    );
-                if (is_enabled) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f);
-                if (ImGui::BeginCombo("###network_iface", selected_display_name)) {
-                    if (should_refresh) {
-                        iface_mgr->refresh();
-                        should_refresh = false;
-                    }
-                    int i = 0;
-                    for (auto& iface : iface_mgr->ifaces) {
-                        bool is_selected = iface_mgr->is_current((*iface));
-                        ImGui::PushID(i++);
-                        if (ImGui::Selectable(iface->friendlyname.c_str(), is_selected)) {
-                            if (!is_enabled) iface_mgr->select((*iface));
-                        }
-                        if (is_selected) ImGui::SetItemDefaultFocus();
-                        ImGui::PopID();
-                    }
-                    ImGui::EndCombo();
-                } else {
-                    should_refresh = true;
-                }
-                if (is_enabled) ImGui::PopStyleVar();
-
-                ImGui::NextColumn();
-            }
-        }
-
-        ImGui::Columns(1);
-
-        ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
-        ImGui::Separator();
-        ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y));
-
-        Hyperlink("Help", "https://xemu.app/docs/networking/");
-
-        ImGui::SameLine();
-        ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)*g_ui_scale);
-        ImGui::SetItemDefaultFocus();
-        if (ImGui::Button(is_enabled ? "Disable" : "Enable", ImVec2(120*g_ui_scale, 0))) {
-            if (!is_enabled) {
-                xemu_settings_set_string(&g_config.net.udp.remote_addr, remote_addr);
-                xemu_settings_set_string(&g_config.net.udp.bind_addr, local_addr);
-                xemu_net_enable();
-            } else {
-                xemu_net_disable();
-            }
-        }
-
-        ImGui::End();
-    }
-};
-
-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()
-    {
-        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 = 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()
-    {
-    }
-
-    void 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_ui_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_ui_scale;
-        ImGui::SetNextItemWidth(item_width);
-        ImGui::PushFont(g_fixed_width_font);
-        if (ImGui::InputText("###UserToken", token_buf, sizeof(token_buf), 0)) {
-            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_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_ui_scale);
-
-        ImGui::SetItemDefaultFocus();
-        if (ImGui::Button("Send", ImVec2(120*g_ui_scale, 0))) {
-            did_send = true;
-            send_result = report.Send();
-            if (send_result) {
-                is_open = false;
-                xemu_settings_set_string(&g_config.general.user_token, token_buf);
-            }
-        }
-
-        ImGui::End();
-    }
-};
-
-#include <math.h>
-
-float mix(float a, float b, float t)
-{
-    return a*(1.0-t) + (b-a)*t;
-}
-
-class DebugApuWindow
-{
-public:
-    bool is_open;
-
-    DebugApuWindow()
-    {
-        is_open = false;
-    }
-
-    ~DebugApuWindow()
-    {
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        ImGui::SetNextWindowContentSize(ImVec2(600.0f*g_ui_scale, 0.0f));
-        if (!ImGui::Begin("Audio Debug", &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;
-
-        // Color buttons, demonstrate using PushID() to add unique identifier in the ID stack, and changing style.
-        ImGui::PushFont(g_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_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_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();
-
-        ImGui::Separator();
-
-        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;
-        }
-    }
-};
-
-class DebugVideoWindow
-{
-public:
-    bool is_open;
-    bool transparent;
-
-    DebugVideoWindow()
-    {
-        is_open = false;
-        transparent = false;
-    }
-
-    ~DebugVideoWindow()
-    {
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        float alpha = transparent ? 0.2 : 1.0;
-        PushWindowTransparencySettings(transparent, 0.2);
-        ImGui::SetNextWindowSize(ImVec2(600.0f*g_ui_scale, 150.0f*g_ui_scale), ImGuiCond_Once);
-        if (ImGui::Begin("Video Debug", &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;
-            ImPlot::SetNextPlotLimitsX(x_start, x_end, ImGuiCond_Always);
-            ImPlot::SetNextPlotLimitsY(0, 65, ImGuiCond_Always);
-
-            float plot_width = 0.5 * (ImGui::GetWindowSize().x -
-                                      2 * ImGui::GetStyle().WindowPadding.x -
-                                      ImGui::GetStyle().ItemSpacing.x);
-
-            ImGui::SetNextWindowBgAlpha(alpha);
-            if (ImPlot::BeginPlot("##ScrollingFPS", NULL, NULL, ImVec2(plot_width,75*g_ui_scale), 0, rt_axis, rt_axis | ImPlotAxisFlags_Lock)) {
-                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::AnnotateClamped(x_start, 65, ImVec2(0,0), ImPlot::GetLastItemColor(), "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::SetNextPlotLimitsX(x_start, x_end, ImGuiCond_Always);
-            ImPlot::SetNextPlotLimitsY(0, 100, ImGuiCond_Always);
-            ImPlot::PushStyleColor(ImPlotCol_Line, ImPlot::GetColormapColor(1));
-            ImGui::SetNextWindowBgAlpha(alpha);
-            if (ImPlot::BeginPlot("##ScrollingMSPF", NULL, NULL, ImVec2(plot_width,75*g_ui_scale), 0, rt_axis, rt_axis | ImPlotAxisFlags_Lock)) {
-                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::AnnotateClamped(x_start, 100, ImVec2(0,0), ImPlot::GetLastItemColor(), "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")) {
-                ImPlot::SetNextPlotLimitsX(x_start, x_end, ImGuiCond_Always);
-                ImPlot::SetNextPlotLimitsY(0, 1500, ImGuiCond_Always);
-                ImGui::SetNextWindowBgAlpha(alpha);
-                if (ImPlot::BeginPlot("##ScrollingDraws", NULL, NULL, ImVec2(-1,500*g_ui_scale), 0, rt_axis, rt_axis | ImPlotAxisFlags_Lock)) {
-                    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)) {
-                transparent = !transparent;
-            }
-
-            ImPlot::PopStyleVar(2);
-        }
-        ImGui::End();
-        ImGui::PopStyleColor(5);
-    }
-};
-
-#if defined(_WIN32)
-class AutoUpdateWindow
-{
-protected:
-    Updater updater;
-
-public:
-    bool is_open;
-
-    AutoUpdateWindow()
-    {
-        is_open = false;
-    }
-
-    ~AutoUpdateWindow()
-    {
-    }
-
-    void check_for_updates_and_prompt_if_available()
-    {
-        updater.check_for_update([this](){
-            is_open |= updater.is_update_available();
-        });
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-        ImGui::SetNextWindowContentSize(ImVec2(550.0f*g_ui_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_ui_scale;
-        float bw = w + (10)*g_ui_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();
-    }
-};
-#endif
-
-static MonitorWindow monitor_window;
-static DebugApuWindow apu_window;
-static DebugVideoWindow video_window;
-static InputWindow input_window;
-static NetworkWindow network_window;
-static AboutWindow about_window;
-static SettingsWindow settings_window;
-static CompatibilityReporter compatibility_reporter_window;
-static NotificationManager notification_manager;
-#if defined(_WIN32)
-static AutoUpdateWindow update_window;
-#endif
-static std::deque<const char *> g_errors;
-
-#ifdef CONFIG_RENDERDOC
-static bool capture_renderdoc_frame = false;
-#endif
-
-class FirstBootWindow
-{
-public:
-    bool is_open;
-
-    FirstBootWindow()
-    {
-        is_open = false;
-    }
-
-    ~FirstBootWindow()
-    {
-    }
-
-    void Draw()
-    {
-        if (!is_open) return;
-
-        ImVec2 size(400*g_ui_scale, 300*g_ui_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_ui_scale);
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256*g_ui_scale)/2);
-
-        ImTextureID id = (ImTextureID)(intptr_t)render_to_fbo(logo_fbo);
-        float t_w = 256.0;
-        float t_h = 256.0;
-        float x_off = 0;
-        ImGui::Image(id,
-            ImVec2((t_w-x_off)*g_ui_scale, t_h*g_ui_scale),
-            ImVec2(x_off/t_w, t_h/t_h),
-            ImVec2(t_w/t_w, 0));
-        if (ImGui::IsItemClicked()) {
-            time_start = SDL_GetTicks();
-        }
-        render_logo(now, 0x42e335ff, 0x42e335ff, 0x00000000);
-        render_to_default_fb();
-
-        ImGui::SetCursorPosY(ImGui::GetCursorPosY()-100*g_ui_scale);
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(xemu_version).x)/2);
-        ImGui::Text("%s", xemu_version);
-
-        ImGui::SetCursorPosX(10*g_ui_scale);
-        ImGui::Dummy(ImVec2(0,20*g_ui_scale));
-
-        const char *msg = "To get started, please configure machine settings.";
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2);
-        ImGui::Text("%s", msg);
-
-        ImGui::Dummy(ImVec2(0,20*g_ui_scale));
-        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120*g_ui_scale)/2);
-        if (ImGui::Button("Settings", ImVec2(120*g_ui_scale, 0))) {
-            settings_window.is_open = true; // FIXME
-        }
-        ImGui::Dummy(ImVec2(0,20*g_ui_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();
-    }
-};
-
-static bool is_shortcut_key_pressed(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 && io.KeysDown[scancode] && (io.KeysDownDuration[scancode] == 0.0);
-}
-
-static void action_eject_disc(void)
-{
-    xemu_settings_set_string(&g_config.sys.files.dvd_path, "");
-    xemu_eject_disc();
-}
-
-static void action_load_disc(void)
-{
-    const char *iso_file_filters = ".iso Files\0*.iso\0All Files\0*.*\0";
-    const char *new_disc_path = paused_file_open(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);
-}
-
-static void action_toggle_pause(void)
-{
-    if (runstate_is_running()) {
-        vm_stop(RUN_STATE_PAUSED);
-    } else {
-        vm_start();
-    }
-}
-
-static void action_reset(void)
-{
-    qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
-}
-
-static void action_shutdown(void)
-{
-    qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI);
-}
-
-
-static bool is_key_pressed(int scancode)
-{
-    ImGuiIO& io = ImGui::GetIO();
-    return io.KeysDown[scancode] && (io.KeysDownDuration[scancode] == 0.0);
-}
-
-static void process_keyboard_shortcuts(void)
-{
-    if (is_shortcut_key_pressed(SDL_SCANCODE_E)) {
-        action_eject_disc();
-    }
-
-    if (is_shortcut_key_pressed(SDL_SCANCODE_O)) {
-        action_load_disc();
-    }
-
-    if (is_shortcut_key_pressed(SDL_SCANCODE_P)) {
-        action_toggle_pause();
-    }
-
-    if (is_shortcut_key_pressed(SDL_SCANCODE_R)) {
-        action_reset();
-    }
-
-    if (is_shortcut_key_pressed(SDL_SCANCODE_Q)) {
-        action_shutdown();
-    }
-
-    if (is_key_pressed(SDL_SCANCODE_GRAVE)) {
-        monitor_window.toggle_open();
-    }
-
-#if defined(DEBUG_NV2A_GL) && defined(CONFIG_RENDERDOC)
-    if (is_key_pressed(SDL_SCANCODE_F10)) {
-        nv2a_dbg_renderdoc_capture_frames(1);
-    }
-#endif
-}
-
-#if defined(__APPLE__)
-#define SHORTCUT_MENU_TEXT(c) "Cmd+" #c
-#else
-#define SHORTCUT_MENU_TEXT(c) "Ctrl+" #c
-#endif
-
-static void ShowMainMenu()
-{
-    bool running = runstate_is_running();
-
-    if (ImGui::BeginMainMenuBar())
-    {
-        if (ImGui::BeginMenu("Machine"))
-        {
-            if (ImGui::MenuItem("Eject Disc", SHORTCUT_MENU_TEXT(E))) {
-                action_eject_disc();
-            }
-            if (ImGui::MenuItem("Load Disc...", SHORTCUT_MENU_TEXT(O))) {
-                action_load_disc();
-            }
-
-            ImGui::Separator();
-
-            ImGui::MenuItem("Input",    NULL, &input_window.is_open);
-            ImGui::MenuItem("Network",  NULL, &network_window.is_open);
-            ImGui::MenuItem("Settings", NULL, &settings_window.is_open);
-
-            ImGui::Separator();
-
-            if (ImGui::MenuItem(running ? "Pause" : "Run", SHORTCUT_MENU_TEXT(P))) {
-                action_toggle_pause();
-            }
-            if (ImGui::MenuItem("Reset", SHORTCUT_MENU_TEXT(R))) {
-                action_reset();
-            }
-            if (ImGui::MenuItem("Shutdown", SHORTCUT_MENU_TEXT(Q))) {
-                action_shutdown();
-            }
-            ImGui::EndMenu();
-        }
-
-        if (ImGui::BeginMenu("View"))
-        {
-            int ui_scale_combo = g_ui_scale - 1.0;
-            if (ui_scale_combo < 0) ui_scale_combo = 0;
-            if (ui_scale_combo > 1) ui_scale_combo = 1;
-            if (ImGui::Combo("UI Scale", &ui_scale_combo, "1x\0" "2x\0")) {
-                g_ui_scale = ui_scale_combo + 1;
-                g_config.display.ui.scale = g_ui_scale;
-                g_trigger_style_update = true;
-            }
-
-            int rendering_scale = nv2a_get_surface_scale_factor() - 1;
-            if (ImGui::Combo("Rendering 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);
-            }
-
-            if (ImGui::Combo(
-                    "Scaling 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.is_open);
-            ImGui::MenuItem("Video", NULL, &video_window.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();
-            ImGui::MenuItem("About", NULL, &about_window.is_open);
-            ImGui::EndMenu();
-        }
-
-        g_main_menu_height = ImGui::GetWindowHeight();
-        ImGui::EndMainMenuBar();
-    }
-}
-
-static void InitializeStyle()
-{
-    ImGuiIO& io = ImGui::GetIO();
-
-    io.Fonts->Clear();
-
-    ImFontConfig roboto_font_cfg = ImFontConfig();
-    roboto_font_cfg.FontDataOwnedByAtlas = false;
-    io.Fonts->AddFontFromMemoryTTF((void*)roboto_medium_data, roboto_medium_size, 16*g_ui_scale, &roboto_font_cfg);
-
-    ImFontConfig font_cfg = ImFontConfig();
-    font_cfg.OversampleH = font_cfg.OversampleV = 1;
-    font_cfg.PixelSnapH = true;
-    font_cfg.SizePixels = 13.0f*g_ui_scale;
-    g_fixed_width_font = io.Fonts->AddFontDefault(&font_cfg);
-
-    ImGui_ImplOpenGL3_CreateFontsTexture();
-
-    ImGuiStyle style;
-    style.WindowRounding = 8.0;
-    style.FrameRounding = 8.0;
-    style.GrabRounding = 12.0;
-    style.PopupRounding = 5.0;
-    style.ScrollbarRounding = 12.0;
-    style.FramePadding.x = 10;
-    style.FramePadding.y = 4;
-    style.WindowBorderSize = 0;
-    style.PopupBorderSize = 0;
-    style.FrameBorderSize = 0;
-    style.TabBorderSize = 0;
-    ImGui::GetStyle() = style;
-    ImGui::GetStyle().ScaleAllSizes(g_ui_scale);
-
-    // Set default theme, override
-    ImGui::StyleColorsDark();
-
-    ImVec4* colors = ImGui::GetStyle().Colors;
-    colors[ImGuiCol_Text]                   = ImVec4(0.86f, 0.93f, 0.89f, 0.78f);
-    colors[ImGuiCol_TextDisabled]           = ImVec4(0.86f, 0.93f, 0.89f, 0.28f);
-    colors[ImGuiCol_WindowBg]               = ImVec4(0.06f, 0.06f, 0.06f, 0.98f);
-    colors[ImGuiCol_ChildBg]                = ImVec4(0.16f, 0.16f, 0.16f, 0.58f);
-    colors[ImGuiCol_PopupBg]                = ImVec4(0.16f, 0.16f, 0.16f, 0.90f);
-    colors[ImGuiCol_Border]                 = ImVec4(0.11f, 0.11f, 0.11f, 0.60f);
-    colors[ImGuiCol_BorderShadow]           = ImVec4(0.16f, 0.16f, 0.16f, 0.00f);
-    colors[ImGuiCol_FrameBg]                = ImVec4(0.16f, 0.16f, 0.16f, 1.00f);
-    colors[ImGuiCol_FrameBgHovered]         = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
-    colors[ImGuiCol_FrameBgActive]          = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
-    colors[ImGuiCol_TitleBg]                = ImVec4(0.20f, 0.51f, 0.18f, 1.00f);
-    colors[ImGuiCol_TitleBgActive]          = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
-    colors[ImGuiCol_TitleBgCollapsed]       = ImVec4(0.16f, 0.16f, 0.16f, 0.75f);
-    colors[ImGuiCol_MenuBarBg]              = ImVec4(0.14f, 0.14f, 0.14f, 0.00f);
-    colors[ImGuiCol_ScrollbarBg]            = ImVec4(0.16f, 0.16f, 0.16f, 1.00f);
-    colors[ImGuiCol_ScrollbarGrab]          = ImVec4(0.20f, 0.51f, 0.18f, 1.00f);
-    colors[ImGuiCol_ScrollbarGrabHovered]   = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
-    colors[ImGuiCol_ScrollbarGrabActive]    = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
-    colors[ImGuiCol_CheckMark]              = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
-    colors[ImGuiCol_SliderGrab]             = ImVec4(0.26f, 0.26f, 0.26f, 1.00f);
-    colors[ImGuiCol_SliderGrabActive]       = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
-    colors[ImGuiCol_Button]                 = ImVec4(0.36f, 0.36f, 0.36f, 1.00f);
-    colors[ImGuiCol_ButtonHovered]          = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
-    colors[ImGuiCol_ButtonActive]           = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
-    colors[ImGuiCol_Header]                 = ImVec4(0.28f, 0.71f, 0.25f, 0.76f);
-    colors[ImGuiCol_HeaderHovered]          = ImVec4(0.28f, 0.71f, 0.25f, 0.86f);
-    colors[ImGuiCol_HeaderActive]           = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
-    colors[ImGuiCol_Separator]              = ImVec4(0.11f, 0.11f, 0.11f, 0.60f);
-    colors[ImGuiCol_SeparatorHovered]       = ImVec4(0.13f, 0.87f, 0.16f, 0.78f);
-    colors[ImGuiCol_SeparatorActive]        = ImVec4(0.25f, 0.75f, 0.10f, 1.00f);
-    colors[ImGuiCol_ResizeGrip]             = ImVec4(0.47f, 0.83f, 0.49f, 0.04f);
-    colors[ImGuiCol_ResizeGripHovered]      = ImVec4(0.28f, 0.71f, 0.25f, 0.78f);
-    colors[ImGuiCol_ResizeGripActive]       = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
-    colors[ImGuiCol_Tab]                    = ImVec4(0.26f, 0.67f, 0.23f, 0.95f);
-    colors[ImGuiCol_TabHovered]             = ImVec4(0.28f, 0.71f, 0.25f, 0.86f);
-    colors[ImGuiCol_TabActive]              = ImVec4(0.26f, 0.66f, 0.23f, 1.00f);
-    colors[ImGuiCol_TabUnfocused]           = ImVec4(0.21f, 0.54f, 0.19f, 0.99f);
-    colors[ImGuiCol_TabUnfocusedActive]     = ImVec4(0.24f, 0.60f, 0.21f, 1.00f);
-    colors[ImGuiCol_PlotLines]              = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
-    colors[ImGuiCol_PlotLinesHovered]       = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
-    colors[ImGuiCol_PlotHistogram]          = ImVec4(0.86f, 0.93f, 0.89f, 0.63f);
-    colors[ImGuiCol_PlotHistogramHovered]   = ImVec4(0.28f, 0.71f, 0.25f, 1.00f);
-    colors[ImGuiCol_TextSelectedBg]         = ImVec4(0.28f, 0.71f, 0.25f, 0.43f);
-    colors[ImGuiCol_DragDropTarget]         = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
-    colors[ImGuiCol_NavHighlight]           = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
-    colors[ImGuiCol_NavWindowingHighlight]  = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
-    colors[ImGuiCol_NavWindowingDimBg]      = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
-    colors[ImGuiCol_ModalWindowDimBg]       = ImVec4(0.16f, 0.16f, 0.16f, 0.73f);
-}
-
-/* External interface, called from ui/xemu.c which handles SDL main loop */
-static FirstBootWindow first_boot_window;
-static SDL_Window *g_sdl_window;
-
-void xemu_hud_init(SDL_Window* window, void* sdl_gl_context)
-{
-    xemu_monitor_init();
-
-    initialize_custom_ui_rendering();
-
-    // 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");
-
-    first_boot_window.is_open = g_config.general.show_welcome;
-
-    int ui_scale_int = g_config.display.ui.scale;
-    if (ui_scale_int < 1) ui_scale_int = 1;
-    g_ui_scale = ui_scale_int;
-
-    g_sdl_window = window;
-
-    ImPlot::CreateContext();
-
-#if defined(_WIN32)
-    if (!g_config.general.show_welcome && g_config.general.updates.check) {
-        update_window.check_for_updates_and_prompt_if_available();
-    }
-#endif
-}
-
-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_render(void)
-{
-    uint32_t now = SDL_GetTicks();
-    bool ui_wakeup = false;
-
-    // Combine all controller states to allow any controller to navigate
-    uint32_t 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;
-        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 guide button is pressed, wake the ui
-    bool menu_button = false;
-    if (buttons & CONTROLLER_BUTTON_GUIDE) {
-        ui_wakeup = true;
-        menu_button = true;
-    }
-
-    // Allow controllers without a guide button to also work
-    if ((buttons & CONTROLLER_BUTTON_BACK) &&
-        (buttons & CONTROLLER_BUTTON_START)) {
-        ui_wakeup = true;
-        menu_button = true;
-    }
-
-    // If the mouse is moved, wake the ui
-    static ImVec2 last_mouse_pos = ImVec2();
-    ImVec2 current_mouse_pos = ImGui::GetMousePos();
-    if ((current_mouse_pos.x != last_mouse_pos.x) ||
-        (current_mouse_pos.y != last_mouse_pos.y)) {
-        last_mouse_pos = current_mouse_pos;
-        ui_wakeup = true;
-    }
-
-    // If mouse capturing is enabled (we are in a dialog), ensure the UI is alive
-    bool controller_focus_capture = false;
-    ImGuiIO& io = ImGui::GetIO();
-    if (io.NavActive) {
-        ui_wakeup = true;
-        controller_focus_capture = true;
-    }
-
-    // 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);
-
-    if (g_trigger_style_update) {
-        InitializeStyle();
-        g_trigger_style_update = false;
-    }
-
-    // Start the Dear ImGui frame
-    ImGui_ImplOpenGL3_NewFrame();
-
-    // Override SDL2 implementation gamecontroller interface
-    io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad;
-    ImGui_ImplSDL2_NewFrame(g_sdl_window);
-    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
-    io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
-
-    // Update gamepad inputs (from imgui_impl_sdl.cpp)
-    memset(io.NavInputs, 0, sizeof(io.NavInputs));
-    #define MAP_BUTTON(NAV_NO, BUTTON_NO)       { io.NavInputs[NAV_NO] = (buttons & BUTTON_NO) ? 1.0f : 0.0f; }
-    #define MAP_ANALOG(NAV_NO, AXIS_NO, V0, V1) { float vn = (float)(axis[AXIS_NO] - V0) / (float)(V1 - V0); if (vn > 1.0f) vn = 1.0f; if (vn > 0.0f && io.NavInputs[NAV_NO] < vn) io.NavInputs[NAV_NO] = vn; }
-    const int thumb_dead_zone = 8000;           // SDL_gamecontroller.h suggests using this value.
-    MAP_BUTTON(ImGuiNavInput_Activate,      CONTROLLER_BUTTON_A);               // Cross / A
-    MAP_BUTTON(ImGuiNavInput_Cancel,        CONTROLLER_BUTTON_B);               // Circle / B
-    MAP_BUTTON(ImGuiNavInput_Input,         CONTROLLER_BUTTON_Y);               // Triangle / Y
-    MAP_BUTTON(ImGuiNavInput_DpadLeft,      CONTROLLER_BUTTON_DPAD_LEFT);       // D-Pad Left
-    MAP_BUTTON(ImGuiNavInput_DpadRight,     CONTROLLER_BUTTON_DPAD_RIGHT);      // D-Pad Right
-    MAP_BUTTON(ImGuiNavInput_DpadUp,        CONTROLLER_BUTTON_DPAD_UP);         // D-Pad Up
-    MAP_BUTTON(ImGuiNavInput_DpadDown,      CONTROLLER_BUTTON_DPAD_DOWN);       // D-Pad Down
-    MAP_BUTTON(ImGuiNavInput_FocusPrev,     CONTROLLER_BUTTON_WHITE);           // L1 / LB
-    MAP_BUTTON(ImGuiNavInput_FocusNext,     CONTROLLER_BUTTON_BLACK);           // R1 / RB
-    MAP_BUTTON(ImGuiNavInput_TweakSlow,     CONTROLLER_BUTTON_WHITE);           // L1 / LB
-    MAP_BUTTON(ImGuiNavInput_TweakFast,     CONTROLLER_BUTTON_BLACK);           // R1 / RB
-
-    // Allow Guide and "Back+Start" buttons to act as Menu button
-    if (menu_button) {
-        io.NavInputs[ImGuiNavInput_Menu] = 1.0;
-    }
-
-    MAP_ANALOG(ImGuiNavInput_LStickLeft,    CONTROLLER_AXIS_LSTICK_X, -thumb_dead_zone, -32768);
-    MAP_ANALOG(ImGuiNavInput_LStickRight,   CONTROLLER_AXIS_LSTICK_X, +thumb_dead_zone, +32767);
-    MAP_ANALOG(ImGuiNavInput_LStickUp,      CONTROLLER_AXIS_LSTICK_Y, +thumb_dead_zone, +32767);
-    MAP_ANALOG(ImGuiNavInput_LStickDown,    CONTROLLER_AXIS_LSTICK_Y, -thumb_dead_zone, -32767);
-
-    ImGui::NewFrame();
-    process_keyboard_shortcuts();
-
-#if defined(DEBUG_NV2A_GL) && defined(CONFIG_RENDERDOC)
-    if (capture_renderdoc_frame) {
-        nv2a_dbg_renderdoc_capture_frames(1);
-        capture_renderdoc_frame = false;
-    }
-#endif
-
-    bool show_main_menu = true;
-
-    if (first_boot_window.is_open) {
-        show_main_menu = false;
-    }
-
-    if (show_main_menu) {
-        // 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;
-        if (ui_wakeup) {
-            last_check = now;
-        }
-        if ((now-last_check) > timeout) {
-            float t = fmin((float)((now-last_check)-timeout)/fade_duration, 1.0);
-            alpha = 1.0-t;
-            if (t >= 1.0) {
-                alpha = 0.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;
-        }
-    }
-
-    first_boot_window.Draw();
-    input_window.Draw();
-    settings_window.Draw();
-    monitor_window.Draw();
-    apu_window.Draw();
-    video_window.Draw();
-    about_window.Draw();
-    network_window.Draw();
-    compatibility_reporter_window.Draw();
-    notification_manager.Draw();
-#if defined(_WIN32)
-    update_window.Draw();
-#endif
-
-    // Very rudimentary error notification API
-    if (g_errors.size() > 0) {
-        ImGui::OpenPopup("Error");
-    }
-    if (ImGui::BeginPopupModal("Error", NULL, ImGuiWindowFlags_AlwaysAutoResize))
-    {
-        ImGui::Text("%s", g_errors[0]);
-        ImGui::Dummy(ImVec2(0,16));
-        ImGui::SetItemDefaultFocus();
-        ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10));
-        if (ImGui::Button("Ok", ImVec2(120, 0))) {
-            ImGui::CloseCurrentPopup();
-            free((void*)g_errors[0]);
-            g_errors.pop_front();
-        }
-        ImGui::EndPopup();
-    }
-
-    ImGui::Render();
-    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
-}
-
-/* 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)
-{
-    g_errors.push_back(strdup(msg));
-}
diff --git a/ui/xemu-input.c b/ui/xemu-input.c
index e5060c3ac4..29ee3d2737 100644
--- a/ui/xemu-input.c
+++ b/ui/xemu-input.c
@@ -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;
diff --git a/ui/xemu-net.c b/ui/xemu-net.c
index a6cd9efb22..9d2bdce3f8 100644
--- a/ui/xemu-net.c
+++ b/ui/xemu-net.c
@@ -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;
diff --git a/ui/xemu-os-utils.h b/ui/xemu-os-utils.h
index e95af1f31b..d2dde9370d 100644
--- a/ui/xemu-os-utils.h
+++ b/ui/xemu-os-utils.h
@@ -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
diff --git a/ui/xemu-settings.cc b/ui/xemu-settings.cc
index a591214338..42f2cfbaf1 100644
--- a/ui/xemu-settings.cc
+++ b/ui/xemu-settings.cc
@@ -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);
+}
diff --git a/ui/xemu-settings.h b/ui/xemu-settings.h
index 0175409d4e..c6ba76f1ff 100644
--- a/ui/xemu-settings.h
+++ b/ui/xemu-settings.h
@@ -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
diff --git a/ui/xemu-shaders.c b/ui/xemu-shaders.c
deleted file mode 100644
index 04279e412d..0000000000
--- a/ui/xemu-shaders.c
+++ /dev/null
@@ -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;
-}
diff --git a/ui/xemu-shaders.h b/ui/xemu-shaders.h
deleted file mode 100644
index 34bf1b8ad3..0000000000
--- a/ui/xemu-shaders.h
+++ /dev/null
@@ -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
diff --git a/ui/xemu-update.cc b/ui/xemu-update.cc
deleted file mode 100644
index 88aadb3514..0000000000
--- a/ui/xemu-update.cc
+++ /dev/null
@@ -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);
-}
diff --git a/ui/xemu-update.h b/ui/xemu-update.h
deleted file mode 100644
index b124ba18c8..0000000000
--- a/ui/xemu-update.h
+++ /dev/null
@@ -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
diff --git a/ui/xemu.c b/ui/xemu.c
index 466115d772..5f86b9bda9 100644
--- a/ui/xemu.c
+++ b/ui/xemu.c
@@ -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)
diff --git a/ui/xui/actions.cc b/ui/xui/actions.cc
new file mode 100644
index 0000000000..e4d9e49005
--- /dev/null
+++ b/ui/xui/actions.cc
@@ -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;
+}
\ No newline at end of file
diff --git a/ui/xui/actions.hh b/ui/xui/actions.hh
new file mode 100644
index 0000000000..2ac680af78
--- /dev/null
+++ b/ui/xui/actions.hh
@@ -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();
diff --git a/ui/xui/animation.cc b/ui/xui/animation.cc
new file mode 100644
index 0000000000..a947d6f3dd
--- /dev/null
+++ b/ui/xui/animation.cc
@@ -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;
+}
diff --git a/ui/xui/animation.hh b/ui/xui/animation.hh
new file mode 100644
index 0000000000..f0f213e49a
--- /dev/null
+++ b/ui/xui/animation.hh
@@ -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();
+};
diff --git a/ui/xui/common.hh b/ui/xui/common.hh
new file mode 100644
index 0000000000..c937ed5deb
--- /dev/null
+++ b/ui/xui/common.hh
@@ -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;
diff --git a/ui/xui/compat.cc b/ui/xui/compat.cc
new file mode 100644
index 0000000000..d0405389f5
--- /dev/null
+++ b/ui/xui/compat.cc
@@ -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;
diff --git a/ui/xui/compat.hh b/ui/xui/compat.hh
new file mode 100644
index 0000000000..edff540a84
--- /dev/null
+++ b/ui/xui/compat.hh
@@ -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;
diff --git a/ui/xui/debug.cc b/ui/xui/debug.cc
new file mode 100644
index 0000000000..392b4f35ab
--- /dev/null
+++ b/ui/xui/debug.cc
@@ -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;
diff --git a/ui/xui/debug.hh b/ui/xui/debug.hh
new file mode 100644
index 0000000000..92671dceff
--- /dev/null
+++ b/ui/xui/debug.hh
@@ -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;
diff --git a/ui/xui/font-manager.cc b/ui/xui/font-manager.cc
new file mode 100644
index 0000000000..6dadbc74b0
--- /dev/null
+++ b/ui/xui/font-manager.cc
@@ -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;
+    }
+}
diff --git a/ui/xui/font-manager.hh b/ui/xui/font-manager.hh
new file mode 100644
index 0000000000..5787f2d4ad
--- /dev/null
+++ b/ui/xui/font-manager.hh
@@ -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;
diff --git a/ui/xui/gl-helpers.cc b/ui/xui/gl-helpers.cc
new file mode 100644
index 0000000000..34f04b9395
--- /dev/null
+++ b/ui/xui/gl-helpers.cc
@@ -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);
+    }
+}
diff --git a/ui/xui/gl-helpers.hh b/ui/xui/gl-helpers.hh
new file mode 100644
index 0000000000..d51985cf71
--- /dev/null
+++ b/ui/xui/gl-helpers.hh
@@ -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);
diff --git a/ui/xui/input-manager.cc b/ui/xui/input-manager.cc
new file mode 100644
index 0000000000..aaf64e4237
--- /dev/null
+++ b/ui/xui/input-manager.cc
@@ -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
+}
diff --git a/ui/xui/input-manager.hh b/ui/xui/input-manager.hh
new file mode 100644
index 0000000000..4d604dae76
--- /dev/null
+++ b/ui/xui/input-manager.hh
@@ -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;
diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc
new file mode 100644
index 0000000000..78f435956f
--- /dev/null
+++ b/ui/xui/main-menu.cc
@@ -0,0 +1,1142 @@
+//
+// 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 "scene-manager.hh"
+#include "widgets.hh"
+#include "main-menu.hh"
+#include "font-manager.hh"
+#include "input-manager.hh"
+#include "viewport-manager.hh"
+#include "xemu-hud.h"
+#include "misc.hh"
+#include "gl-helpers.hh"
+#include "reporting.hh"
+
+#include "../xemu-input.h"
+#include "../xemu-notifications.h"
+#include "../xemu-settings.h"
+#include "../xemu-monitor.h"
+#include "../xemu-version.h"
+#include "../xemu-net.h"
+#include "../xemu-os-utils.h"
+#include "../xemu-xbe.h"
+
+MainMenuTabView::~MainMenuTabView() {}
+void MainMenuTabView::Draw() {}
+
+void MainMenuGeneralView::Draw()
+{
+    SectionTitle("Updates");
+    Toggle("Check for updates", &g_config.general.updates.check,
+           "Check for updates whenever xemu is opened");
+
+    SectionTitle("Performance");
+    Toggle("Hard FPU emulation", &g_config.perf.hard_fpu,
+           "Use hardware-accelerated floating point emulation (requires restart)");
+    // toggle("Cache shaders to disk", &g_config.perf.cache_shaders,
+    //        "Reduce stutter in games by caching previously generated shaders");
+
+    SectionTitle("Miscellaneous");
+    Toggle("Skip startup animation", &g_config.general.skip_boot_anim,
+           "Skip the full Xbox boot animation sequence");
+    FilePicker("Screenshot output directory", &g_config.general.screenshot_dir,
+               NULL, true);
+    // toggle("Throttle DVD/HDD speeds", &g_config.general.throttle_io,
+    //        "Limit DVD/HDD throughput to approximate Xbox load times");
+}
+
+void MainMenuInputView::Draw()
+{
+    SectionTitle("Controllers");
+    ImGui::PushFont(g_font_mgr.m_menu_font_small);
+
+    static int active = 0;
+
+    // Output dimensions of texture
+    float t_w = 512, t_h = 512;
+    // Dimensions of (port+label)s
+    float b_x = 0, b_x_stride = 100, b_y = 400;
+    float b_w = 68, b_h = 81;
+    // Dimensions of controller (rendered at origin)
+    float controller_width  = 477.0f;
+    float controller_height = 395.0f;
+
+    // Setup rendering to fbo for controller and port images
+    controller_fbo->Target();
+    ImTextureID id = (ImTextureID)(intptr_t)controller_fbo->Texture();
+
+    //
+    // Render buttons with icons of the Xbox style port sockets with
+    // circular numbers above them. These buttons can be activated to
+    // configure the associated port, like a tabbed interface.
+    //
+    ImVec4 color_active(0.50, 0.86, 0.54, 0.12);
+    ImVec4 color_inactive(0, 0, 0, 0);
+
+    // Begin a 4-column layout to render the ports
+    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
+                        g_viewport_mgr.Scale(ImVec2(0, 12)));
+    ImGui::Columns(4, "mixed", false);
+
+    const int port_padding = 8;
+    for (int i = 0; i < 4; i++) {
+        bool is_selected = (i == active);
+        bool port_is_bound = (xemu_input_get_bound(i) != NULL);
+
+        // Set an X offset to center the image button within the column
+        ImGui::SetCursorPosX(
+            ImGui::GetCursorPosX() +
+            (int)((ImGui::GetColumnWidth() - b_w * g_viewport_mgr.m_scale -
+                   2 * port_padding * g_viewport_mgr.m_scale) /
+                  2));
+
+        // We are using the same texture for all buttons, but ImageButton
+        // uses the texture as a unique ID. Push a new ID now to resolve
+        // the conflict.
+        ImGui::PushID(i);
+        float x = b_x+i*b_x_stride;
+        ImGui::PushStyleColor(ImGuiCol_Button, is_selected ?
+                                                   color_active :
+                                                   color_inactive);
+        bool activated = ImGui::ImageButton(id,
+            ImVec2(b_w*g_viewport_mgr.m_scale,b_h*g_viewport_mgr.m_scale),
+            ImVec2(x/t_w, (b_y+b_h)/t_h),
+            ImVec2((x+b_w)/t_w, b_y/t_h),
+            port_padding);
+        ImGui::PopStyleColor();
+
+        if (activated) {
+            active = i;
+        }
+
+        uint32_t port_color = 0xafafafff;
+        bool is_hovered = ImGui::IsItemHovered();
+        if (is_hovered) {
+            port_color = 0xffffffff;
+        } else if (is_selected || port_is_bound) {
+            port_color = 0x81dc8a00;
+        }
+
+        RenderControllerPort(x, b_y, i, port_color);
+
+        ImGui::PopID();
+        ImGui::NextColumn();
+    }
+    ImGui::PopStyleVar(); // ItemSpacing
+    ImGui::Columns(1);
+
+    //
+    // Render input device combo
+    //
+
+    // List available input devices
+    const char *not_connected = "Not Connected";
+    ControllerState *bound_state = xemu_input_get_bound(active);
+
+    // Get current controller name
+    const char *name;
+    if (bound_state == NULL) {
+        name = not_connected;
+    } else {
+        name = bound_state->name;
+    }
+
+    ImGui::SetNextItemWidth(-FLT_MIN);
+    if (ImGui::BeginCombo("###InputDevices", name, ImGuiComboFlags_NoArrowButton))
+    {
+        // Handle "Not connected"
+        bool is_selected = bound_state == NULL;
+        if (ImGui::Selectable(not_connected, is_selected)) {
+            xemu_input_bind(active, NULL, 1);
+            bound_state = NULL;
+        }
+        if (is_selected) {
+            ImGui::SetItemDefaultFocus();
+        }
+
+        // Handle all available input devices
+        ControllerState *iter;
+        QTAILQ_FOREACH(iter, &available_controllers, entry) {
+            is_selected = bound_state == iter;
+            ImGui::PushID(iter);
+            const char *selectable_label = iter->name;
+            char buf[128];
+            if (iter->bound >= 0) {
+                snprintf(buf, sizeof(buf), "%s (Port %d)", iter->name, iter->bound+1);
+                selectable_label = buf;
+            }
+            if (ImGui::Selectable(selectable_label, is_selected)) {
+                xemu_input_bind(active, iter, 1);
+                bound_state = iter;
+            }
+            if (is_selected) {
+                ImGui::SetItemDefaultFocus();
+            }
+            ImGui::PopID();
+        }
+
+        ImGui::EndCombo();
+    }
+    DrawComboChevron();
+
+    ImGui::Columns(1);
+
+    //
+    // Add a separator between input selection and controller graphic
+    //
+    ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y / 2));
+
+    //
+    // Render controller image
+    //
+    bool device_selected = false;
+
+    if (bound_state) {
+        device_selected = true;
+        RenderController(0, 0, 0x81dc8a00, 0x0f0f0f00, bound_state);
+    } else {
+        static ControllerState state = { 0 };
+        RenderController(0, 0, 0x1f1f1f00, 0x0f0f0f00, &state);
+    }
+
+    ImVec2 cur = ImGui::GetCursorPos();
+
+    ImVec2 controller_display_size;
+    if (ImGui::GetContentRegionMax().x < controller_width*g_viewport_mgr.m_scale) {
+        controller_display_size.x = ImGui::GetContentRegionMax().x;
+        controller_display_size.y =
+            controller_display_size.x * controller_height / controller_width;
+    } else {
+        controller_display_size =
+            ImVec2(controller_width * g_viewport_mgr.m_scale,
+                   controller_height * g_viewport_mgr.m_scale);
+    }
+
+    ImGui::SetCursorPosX(
+        ImGui::GetCursorPosX() +
+        (int)((ImGui::GetColumnWidth() - controller_display_size.x) / 2.0));
+
+    ImGui::Image(id,
+        controller_display_size,
+        ImVec2(0, controller_height/t_h),
+        ImVec2(controller_width/t_w, 0));
+    ImVec2 pos = ImGui::GetCursorPos();
+    if (!device_selected) {
+        const char *msg = "Please select an available input device";
+        ImVec2 dim = ImGui::CalcTextSize(msg);
+        ImGui::SetCursorPosX(cur.x + (controller_display_size.x-dim.x)/2);
+        ImGui::SetCursorPosY(cur.y + (controller_display_size.y-dim.y)/2);
+        ImGui::Text("%s", msg);
+    }
+
+    controller_fbo->Restore();
+
+    ImGui::PopFont();
+    ImGui::SetCursorPos(pos);
+
+    SectionTitle("Options");
+    Toggle("Auto-bind controllers", &g_config.input.auto_bind,
+           "Bind newly connected controllers to any open port");
+    Toggle("Background controller input capture",
+           &g_config.input.background_input_capture,
+           "Capture even if window is unfocused (requires restart)");
+}
+
+void MainMenuDisplayView::Draw()
+{
+    SectionTitle("Quality");
+    int rendering_scale = nv2a_get_surface_scale_factor() - 1;
+    if (ChevronCombo("Internal resolution scale", &rendering_scale,
+                     "1x\0"
+                     "2x\0"
+                     "3x\0"
+                     "4x\0"
+                     "5x\0"
+                     "6x\0"
+                     "7x\0"
+                     "8x\0"
+                     "9x\0"
+                     "10x\0",
+                     "Increase surface scaling factor for higher quality")) {
+        nv2a_set_surface_scale_factor(rendering_scale+1);
+    }
+
+    SectionTitle("Window");
+    bool fs = xemu_is_fullscreen();
+    if (Toggle("Fullscreen", &fs, "Enable fullscreen now")) {
+        xemu_toggle_fullscreen();
+    }
+    Toggle("Fullscreen on startup",
+           &g_config.display.window.fullscreen_on_startup,
+           "Start xemu in fullscreen when opened");
+    if (ChevronCombo("Window size", &g_config.display.window.startup_size,
+                     "Last Used\0"
+                     "640x480\0"
+                     "1280x720\0"
+                     "1280x800\0"
+                     "1280x960\0"
+                     "1920x1080\0"
+                     "2560x1440\0"
+                     "2560x1600\0"
+                     "2560x1920\0"
+                     "3840x2160\0",
+                     "Select preferred startup window size")) {
+    }
+    Toggle("Vertical refresh sync", &g_config.display.window.vsync,
+           "Sync to screen vertical refresh to reduce tearing artifacts");
+
+    SectionTitle("Interface");
+    Toggle("Show main menu bar", &g_config.display.ui.show_menubar,
+           "Show main menu bar when mouse is activated");
+
+    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 (ChevronCombo("UI scale", &ui_scale_idx,
+                     "Auto\0"
+                     "1x\0"
+                     "2x\0",
+                     "Interface element scale")) {
+        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;
+        }
+    }
+    Toggle("Animations", &g_config.display.ui.use_animations,
+           "Enable xemu user interface animations");
+    ChevronCombo("Display mode", &g_config.display.ui.fit,
+                 "Center\0"
+                 "Scale\0"
+                 "Scale (Widescreen 16:9)\0"
+                 "Scale (4:3)\0"
+                 "Stretch\0",
+                 "Select how the framebuffer should fit or scale into the window");
+}
+
+void MainMenuAudioView::Draw()
+{
+    SectionTitle("Volume");
+    char buf[32];
+    snprintf(buf, sizeof(buf), "Limit output volume (%d%%)",
+             (int)(g_config.audio.volume_limit * 100));
+    Slider("Output volume limit", &g_config.audio.volume_limit, buf);
+
+    SectionTitle("Quality");
+    Toggle("Real-time DSP processing", &g_config.audio.use_dsp,
+           "Enable improved audio accuracy (experimental)");
+
+}
+
+NetworkInterface::NetworkInterface(pcap_if_t *pcap_desc, char *_friendlyname)
+{
+    m_pcap_name = pcap_desc->name;
+    m_description = pcap_desc->description ?: pcap_desc->name;
+    if (_friendlyname) {
+        char *tmp =
+            g_strdup_printf("%s (%s)", _friendlyname, m_description.c_str());
+        m_friendly_name = tmp;
+        g_free((gpointer)tmp);
+    } else {
+        m_friendly_name = m_description;
+    }
+}
+
+NetworkInterfaceManager::NetworkInterfaceManager()
+{
+    m_current_iface = NULL;
+    m_failed_to_load_lib = false;
+}
+
+void NetworkInterfaceManager::Refresh(void)
+{
+    pcap_if_t *alldevs, *iter;
+    char err[PCAP_ERRBUF_SIZE];
+
+    if (xemu_net_is_enabled()) {
+        return;
+    }
+
+#if defined(_WIN32)
+    if (pcap_load_library()) {
+        m_failed_to_load_lib = true;
+        return;
+    }
+#endif
+
+    m_ifaces.clear();
+    m_current_iface = NULL;
+
+    if (pcap_findalldevs(&alldevs, err)) {
+        return;
+    }
+
+    for (iter=alldevs; iter != NULL; iter=iter->next) {
+#if defined(_WIN32)
+        char *friendly_name = get_windows_interface_friendly_name(iter->name);
+        m_ifaces.emplace_back(new NetworkInterface(iter, friendly_name));
+        if (friendly_name) {
+            g_free((gpointer)friendly_name);
+        }
+#else
+        m_ifaces.emplace_back(new NetworkInterface(iter));
+#endif
+        if (!strcmp(g_config.net.pcap.netif, iter->name)) {
+            m_current_iface = m_ifaces.back().get();
+        }
+    }
+
+    pcap_freealldevs(alldevs);
+}
+
+void NetworkInterfaceManager::Select(NetworkInterface &iface)
+{
+    m_current_iface = &iface;
+    xemu_settings_set_string(&g_config.net.pcap.netif,
+                             iface.m_pcap_name.c_str());
+}
+
+bool NetworkInterfaceManager::IsCurrent(NetworkInterface &iface)
+{
+    return &iface == m_current_iface;
+}
+
+MainMenuNetworkView::MainMenuNetworkView()
+{
+    should_refresh = true;
+}
+
+void MainMenuNetworkView::Draw()
+{
+    SectionTitle("Adapter");
+    bool enabled = xemu_net_is_enabled();
+    g_config.net.enable = enabled;
+    if (Toggle("Enable", &g_config.net.enable,
+               enabled ? "Virtual network connected (disable to change network "
+                         "settings)" :
+                         "Connect virtual network cable to machine")) {
+        if (enabled) {
+            xemu_net_disable();
+        } else {
+            xemu_net_enable();
+        }
+    }
+
+    bool appearing = ImGui::IsWindowAppearing();
+    if (enabled) ImGui::BeginDisabled();
+    if (ChevronCombo(
+            "Attached to", &g_config.net.backend,
+            "NAT\0"
+            "UDP Tunnel\0"
+            "Bridged Adapter\0",
+            "Controls what the virtual network controller interfaces with")) {
+        appearing = true;
+    }
+    SectionTitle("Options");
+    switch (g_config.net.backend) {
+    case CONFIG_NET_BACKEND_PCAP:
+        DrawPcapOptions(appearing);
+        break;
+    case CONFIG_NET_BACKEND_NAT:
+        DrawNatOptions(appearing);
+        break;
+    case CONFIG_NET_BACKEND_UDP:
+        DrawUdpOptions(appearing);
+        break;
+    default: break;
+    }
+    if (enabled) ImGui::EndDisabled();
+}
+
+void MainMenuNetworkView::DrawPcapOptions(bool appearing)
+{
+    if (iface_mgr.get() == nullptr) {
+        iface_mgr.reset(new NetworkInterfaceManager());
+        iface_mgr->Refresh();
+    }
+
+    if (iface_mgr->m_failed_to_load_lib) {
+#if defined(_WIN32)
+        const char *msg = "npcap library could not be loaded.\n"
+                          "To use this backend, please install npcap.";
+        ImGui::Text("%s", msg);
+        ImGui::Dummy(ImVec2(0,10*g_viewport_mgr.m_scale));
+        ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120*g_viewport_mgr.m_scale)/2);
+        if (ImGui::Button("Install npcap", ImVec2(120*g_viewport_mgr.m_scale, 0))) {
+            xemu_open_web_browser("https://nmap.org/npcap/");
+        }
+#endif
+    } else {
+        const char *selected_display_name =
+            (iface_mgr->m_current_iface ?
+                 iface_mgr->m_current_iface->m_friendly_name.c_str() :
+                 g_config.net.pcap.netif);
+        float combo_width = ImGui::GetColumnWidth();
+        float combo_size_ratio = 0.5;
+        combo_width *= combo_size_ratio;
+        PrepareComboTitleDescription("Network interface",
+                                     "Host network interface to bridge with",
+                                     combo_size_ratio);
+        ImGui::SetNextItemWidth(combo_width);
+        ImGui::PushFont(g_font_mgr.m_menu_font_small);
+        if (ImGui::BeginCombo("###network_iface", selected_display_name,
+                              ImGuiComboFlags_NoArrowButton)) {
+            if (should_refresh) {
+                iface_mgr->Refresh();
+                should_refresh = false;
+            }
+
+            int i = 0;
+            for (auto &iface : iface_mgr->m_ifaces) {
+                bool is_selected = iface_mgr->IsCurrent((*iface));
+                ImGui::PushID(i++);
+                if (ImGui::Selectable(iface->m_friendly_name.c_str(),
+                                      is_selected)) {
+                    iface_mgr->Select((*iface));
+                }
+                if (is_selected) ImGui::SetItemDefaultFocus();
+                ImGui::PopID();
+            }
+            ImGui::EndCombo();
+        } else {
+            should_refresh = true;
+        }
+        ImGui::PopFont();
+        DrawComboChevron();
+    }
+}
+
+void MainMenuNetworkView::DrawNatOptions(bool appearing)
+{
+    static ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg;
+    WidgetTitleDescriptionItem(
+        "Port Forwarding",
+        "Configure xemu to forward connections to guest on these ports");
+    float p = ImGui::GetFrameHeight() * 0.3;
+    ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(p, p));
+    if (ImGui::BeginTable("port_forward_tbl", 4, flags))
+    {
+        ImGui::TableSetupColumn("Host Port");
+        ImGui::TableSetupColumn("Guest Port");
+        ImGui::TableSetupColumn("Protocol");
+        ImGui::TableSetupColumn("Action");
+        ImGui::TableHeadersRow();
+
+        for (unsigned int row = 0; row < g_config.net.nat.forward_ports_count; row++)
+        {
+            ImGui::TableNextRow();
+
+            ImGui::TableSetColumnIndex(0);
+            ImGui::Text("%d", g_config.net.nat.forward_ports[row].host);
+
+            ImGui::TableSetColumnIndex(1);
+            ImGui::Text("%d", g_config.net.nat.forward_ports[row].guest);
+
+            ImGui::TableSetColumnIndex(2);
+            switch (g_config.net.nat.forward_ports[row].protocol) {
+            case CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_TCP:
+                ImGui::TextUnformatted("TCP"); break;
+            case CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_UDP:
+                ImGui::TextUnformatted("UDP"); break;
+            default: assert(0);
+            }
+
+            ImGui::TableSetColumnIndex(3);
+            ImGui::PushID(row);
+            if (ImGui::Button("Remove")) {
+                remove_net_nat_forward_ports(row);
+            }
+            ImGui::PopID();
+        }
+
+        ImGui::TableNextRow();
+
+        ImGui::TableSetColumnIndex(0);
+        static char buf[8] = {"1234"};
+        ImGui::SetNextItemWidth(ImGui::GetColumnWidth());
+        ImGui::InputText("###hostport", buf, sizeof(buf));
+
+        ImGui::TableSetColumnIndex(1);
+        static char buf2[8] = {"1234"};
+        ImGui::SetNextItemWidth(ImGui::GetColumnWidth());
+        ImGui::InputText("###guestport", buf2, sizeof(buf2));
+
+        ImGui::TableSetColumnIndex(2);
+        static CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol =
+            CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_TCP;
+        assert(sizeof(protocol) >= sizeof(int));
+        ImGui::SetNextItemWidth(ImGui::GetColumnWidth());
+        ImGui::Combo("###protocol", &protocol, "TCP\0UDP\0");
+
+        ImGui::TableSetColumnIndex(3);
+        if (ImGui::Button("Add")) {
+            int host, guest;
+            if (sscanf(buf, "%d", &host) == 1 &&
+                sscanf(buf2, "%d", &guest) == 1) {
+                add_net_nat_forward_ports(host, guest, protocol);
+            }
+        }
+
+        ImGui::EndTable();
+    }
+    ImGui::PopStyleVar();
+}
+
+void MainMenuNetworkView::DrawUdpOptions(bool appearing)
+{
+    if (appearing) {
+        strncpy(remote_addr, g_config.net.udp.remote_addr, sizeof(remote_addr)-1);
+        strncpy(local_addr, g_config.net.udp.bind_addr, sizeof(local_addr)-1);
+    }
+
+    float size_ratio = 0.5;
+    float width = ImGui::GetColumnWidth() * size_ratio;
+    ImGui::PushFont(g_font_mgr.m_menu_font_small);
+    PrepareComboTitleDescription(
+        "Remote Address",
+        "Destination addr:port to forward packets to (1.2.3.4:9968)",
+        size_ratio);
+    ImGui::SetNextItemWidth(width);
+    if (ImGui::InputText("###remote_host", remote_addr, sizeof(remote_addr))) {
+        xemu_settings_set_string(&g_config.net.udp.remote_addr, remote_addr);
+    }
+    PrepareComboTitleDescription(
+        "Bind Address", "Local addr:port to receive packets on (0.0.0.0:9968)",
+        size_ratio);
+    ImGui::SetNextItemWidth(width);
+    if (ImGui::InputText("###local_host", local_addr, sizeof(local_addr))) {
+        xemu_settings_set_string(&g_config.net.udp.bind_addr, local_addr);
+    }
+    ImGui::PopFont();
+}
+
+#if 0
+class MainMenuSnapshotsView : public virtual MainMenuTabView
+{
+protected:
+    GLuint screenshot;
+
+public:
+    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/screenshot.png", &w, &h, &n, 4);
+            assert(n == 4);
+            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);
+        }
+    }
+
+    void snapshotBigButton(const char *name, const char *title_name, GLuint screenshot)
+    {
+        ImGuiStyle &style = ImGui::GetStyle();
+        ImVec2 pos = ImGui::GetCursorPos();
+        ImDrawList *draw_list = ImGui::GetWindowDrawList();
+
+        ImGui::PushFont(g_font_mgr.m_menuFont);
+        const char *icon = ICON_FA_CIRCLE_XMARK;
+        ImVec2 ts_icon = ImGui::CalcTextSize(icon);
+        ts_icon.x += 2*style.FramePadding.x;
+        ImGui::PopFont();
+
+        ImGui::PushFont(g_font_mgr.m_menuFontSmall);
+        ImVec2 ts_sub = ImGui::CalcTextSize(name);
+        ImGui::PopFont();
+
+        ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0));
+        ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, g_viewport_mgr.scale(ImVec2(5, 5)));
+        ImGui::PushFont(g_font_mgr.m_menuFontMedium);
+
+        ImVec2 ts_title = ImGui::CalcTextSize(name);
+        ImVec2 thumbnail_size = g_viewport_mgr.scale(ImVec2(160, 120));
+        ImVec2 thumbnail_pos(style.FramePadding.x, style.FramePadding.y);
+        ImVec2 text_pos(thumbnail_pos.x + thumbnail_size.x + style.FramePadding.x * 2, thumbnail_pos.y);
+        ImVec2 subtext_pos(text_pos.x, text_pos.y + ts_title.y + style.FramePadding.x);
+
+        ImGui::Button("###button", ImVec2(ImGui::GetContentRegionAvail().x, fmax(thumbnail_size.y + style.FramePadding.y * 2,
+                                                                                 ts_title.y + ts_sub.y + style.FramePadding.y * 3)));
+        ImGui::PopFont();
+        const ImVec2 sz = ImGui::GetItemRectSize();
+        const ImVec2 p0 = ImGui::GetItemRectMin();
+        const ImVec2 p1 = ImGui::GetItemRectMax();
+        ts_icon.y = sz.y;
+
+        // Snapshot thumbnail
+        ImGui::SetItemAllowOverlap();
+        ImGui::SameLine();
+        ImGui::SetCursorPosX(pos.x + thumbnail_pos.x);
+        ImGui::SetCursorPosY(pos.y + thumbnail_pos.y);
+        ImGui::Image((ImTextureID)screenshot, thumbnail_size, ImVec2(0,0), ImVec2(1,1));
+
+        draw_list->PushClipRect(p0, p1, true);
+
+        // Snapshot title
+        ImGui::PushFont(g_font_mgr.m_menuFontMedium);
+        draw_list->AddText(ImVec2(p0.x + text_pos.x, p0.y + text_pos.y), IM_COL32(255, 255, 255, 255), name);
+        ImGui::PopFont();
+
+        // Snapshot subtitle
+        ImGui::PushFont(g_font_mgr.m_menuFontSmall);
+        draw_list->AddText(ImVec2(p0.x + subtext_pos.x, p0.y + subtext_pos.y), IM_COL32(255, 255, 255, 200), title_name);
+        ImGui::PopFont();
+
+        draw_list->PopClipRect();
+
+        // Delete button
+        ImGui::SameLine();
+        ImGui::SetCursorPosY(pos.y);
+        ImGui::SetCursorPosX(pos.x + sz.x - ts_icon.x);
+        ImGui::PushFont(g_font_mgr.m_menuFont);
+        ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0));
+        ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
+        ImGui::Button(icon, ts_icon);
+        ImGui::PopStyleColor();
+        ImGui::PopStyleVar(1);
+        ImGui::PopFont();
+        ImGui::PopStyleVar(2);
+    }
+
+    void Draw()
+    {
+        initScreenshot();
+        for (int i = 0; i < 15; i++) {
+            char buf[64];
+            snprintf(buf, sizeof(buf), "%s", "Apr 9 2022 19:44");
+            ImGui::PushID(i);
+            snapshotBigButton(buf, "Halo: Combat Evolved", screenshot);
+            ImGui::PopID();
+        }
+    }
+};
+#endif
+
+MainMenuSystemView::MainMenuSystemView() : m_dirty(false)
+{
+}
+
+void MainMenuSystemView::Draw()
+{
+    const char *rom_file_filters = ".bin Files\0*.bin\0.rom Files\0*.rom\0All Files\0*.*\0";
+    const char *qcow_file_filters = ".qcow2 Files\0*.qcow2\0All Files\0*.*\0";
+
+    if (m_dirty) {
+        ImGui::TextColored(ImVec4(1,0,0,1), "Application restart required to apply settings");
+    }
+
+    SectionTitle("System Configuration");
+
+    if (ChevronCombo(
+            "System Memory", &g_config.sys.mem_limit,
+            "64 MiB (Default)\0""128 MiB\0",
+            "Increase to 128 MiB for debug or homebrew applications")) {
+        m_dirty = true;
+    }
+
+    if (ChevronCombo(
+            "AV Pack", &g_config.sys.avpack,
+            "SCART\0HDTV (Default)\0VGA\0RFU\0S-Video\0Composite\0None\0",
+            "Select the attached AV pack")) {
+        m_dirty = true;
+    }
+
+    SectionTitle("Files");
+    if (FilePicker("Boot ROM", &g_config.sys.files.bootrom_path,
+                   rom_file_filters)) {
+        m_dirty = true;
+    }
+    if (FilePicker("Flash ROM", &g_config.sys.files.flashrom_path,
+                   rom_file_filters)) {
+        m_dirty = true;
+    }
+    if (FilePicker("Hard Disk", &g_config.sys.files.hdd_path,
+                   qcow_file_filters)) {
+        m_dirty = true;
+    }
+    if (FilePicker("EEPROM", &g_config.sys.files.eeprom_path,
+                   rom_file_filters)) {
+        m_dirty = true;
+    }
+}
+
+void MainMenuAboutView::Draw()
+{
+    static const char *build_info_text = NULL;
+    if (build_info_text == NULL) {
+        build_info_text = g_strdup_printf(
+            "Version:      %s\nBranch:       %s\nCommit:       %s\nDate:         %s",
+            xemu_version, xemu_branch, xemu_commit, xemu_date);
+    }
+
+    static const char *sys_info_text = NULL;
+    if (sys_info_text == NULL) {
+        const char *gl_shader_version = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
+        const char *gl_version = (const char*)glGetString(GL_VERSION);
+        const char *gl_renderer = (const char*)glGetString(GL_RENDERER);
+        const char *gl_vendor = (const char*)glGetString(GL_VENDOR);
+        sys_info_text = g_strdup_printf(
+            "CPU:          %s\nOS Platform:  %s\nOS Version:   %s\nManufacturer: %s\n"
+            "GPU Model:    %s\nDriver:       %s\nShader:       %s",
+             xemu_get_cpu_info(), xemu_get_os_platform(), xemu_get_os_info(), gl_vendor,
+             gl_renderer, gl_version, gl_shader_version);
+    }
+
+    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()-75*g_viewport_mgr.m_scale);
+
+    SectionTitle("Build Information");
+    ImGui::PushFont(g_font_mgr.m_fixed_width_font);
+    ImGui::InputTextMultiline("##build_info", (char *)build_info_text,
+                              strlen(build_info_text),
+                              ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 5),
+                              ImGuiInputTextFlags_ReadOnly);
+    ImGui::PopFont();
+
+    SectionTitle("System Information");
+    ImGui::PushFont(g_font_mgr.m_fixed_width_font);
+    ImGui::InputTextMultiline("###systeminformation", (char *)sys_info_text,
+                              strlen(sys_info_text),
+                              ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 8),
+                              ImGuiInputTextFlags_ReadOnly);
+    ImGui::PopFont();
+
+    SectionTitle("Community");
+
+    ImGui::Text("Visit");
+    ImGui::SameLine();
+    if (ImGui::SmallButton("https://xemu.app")) {
+        xemu_open_web_browser("https://xemu.app");
+    }
+    ImGui::SameLine();
+    ImGui::Text("for more information");
+}
+
+MainMenuTabButton::MainMenuTabButton(std::string text, std::string icon, MainMenuTabView *view)
+: m_icon(icon), m_text(text), m_view(view)
+{
+}
+
+MainMenuTabView *MainMenuTabButton::view()
+{
+    return m_view;
+}
+
+bool MainMenuTabButton::Draw(bool selected)
+{
+    ImGuiStyle &style = ImGui::GetStyle();
+
+    ImU32 col = selected ?
+                    ImGui::GetColorU32(style.Colors[ImGuiCol_ButtonHovered]) :
+                    IM_COL32(0, 0, 0, 0);
+
+    ImGui::PushStyleColor(ImGuiCol_Button, col);
+    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, selected ? col : IM_COL32(32, 32, 32, 255));
+    ImGui::PushStyleColor(ImGuiCol_ButtonActive, selected ? col : IM_COL32(32, 32, 32, 255));
+    int p = ImGui::GetTextLineHeight() * 0.5;
+    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(p, p));
+    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
+    ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0.5));
+    ImGui::PushFont(g_font_mgr.m_menu_font);
+
+    ImVec2 button_size = ImVec2(-FLT_MIN, 0);
+    auto text = string_format("%s %s", m_icon.c_str(), m_text.c_str());
+    ImGui::PushID(this);
+    bool status = ImGui::Button(text.c_str(), button_size);
+    ImGui::PopID();
+    ImGui::PopFont();
+    ImGui::PopStyleVar(3);
+    ImGui::PopStyleColor(3);
+    return status;
+}
+
+MainMenuScene::MainMenuScene()
+: m_animation(0.12, 0.12),
+  m_general_button("General",     ICON_FA_GEARS,              &m_general_view),
+  m_input_button("Input",         ICON_FA_GAMEPAD,            &m_input_view),
+  m_display_button("Display",     ICON_FA_TV,                 &m_display_view),
+  m_audio_button("Audio",         ICON_FA_VOLUME_HIGH,        &m_audio_view),
+  m_network_button("Network",     ICON_FA_NETWORK_WIRED,      &m_network_view),
+  // m_snapshots_button("Snapshots", ICON_FA_CLOCK_ROTATE_LEFT,  &m_snapshots_view),
+  m_system_button("System",       ICON_FA_MICROCHIP,          &m_system_view),
+  m_about_button("About",         ICON_FA_CIRCLE_INFO,        &m_about_view)
+{
+    m_had_focus_last_frame = false;
+    m_focus_view = false;
+    m_tabs.push_back(&m_general_button);
+    m_tabs.push_back(&m_input_button);
+    m_tabs.push_back(&m_display_button);
+    m_tabs.push_back(&m_audio_button);
+    m_tabs.push_back(&m_network_button);
+    // m_tabs.push_back(&m_snapshots_button);
+    m_tabs.push_back(&m_system_button);
+    m_tabs.push_back(&m_about_button);
+
+    m_current_view_index = 0;
+    m_next_view_index = m_current_view_index;
+}
+
+void MainMenuScene::ShowGeneral()
+{
+    SetNextViewIndexWithFocus(0);
+}
+void MainMenuScene::ShowInput()
+{
+    SetNextViewIndexWithFocus(1);
+}
+void MainMenuScene::ShowDisplay()
+{
+    SetNextViewIndexWithFocus(2);
+}
+void MainMenuScene::ShowAudio()
+{
+    SetNextViewIndexWithFocus(3);
+}
+void MainMenuScene::ShowNetwork()
+{
+    SetNextViewIndexWithFocus(4);
+}
+// void MainMenuScene::showSnapshots() { SetNextViewIndexWithFocus(5); }
+void MainMenuScene::ShowSystem()
+{
+    SetNextViewIndexWithFocus(5);
+}
+void MainMenuScene::ShowAbout()
+{
+    SetNextViewIndexWithFocus(6);
+}
+
+void MainMenuScene::SetNextViewIndexWithFocus(int i)
+{
+    m_focus_view = true;
+    SetNextViewIndex(i);
+
+    if (!g_scene_mgr.IsDisplayingScene()) {
+        g_scene_mgr.PushScene(*this);
+    }
+}
+
+void MainMenuScene::Show()
+{
+    m_background.Show();
+    m_nav_control_view.Show();
+    m_animation.EaseIn();
+}
+
+void MainMenuScene::Hide()
+{
+    m_background.Hide();
+    m_nav_control_view.Hide();
+    m_animation.EaseOut();
+}
+
+bool MainMenuScene::IsAnimating()
+{
+    return m_animation.IsAnimating();
+}
+
+void MainMenuScene::SetNextViewIndex(int i)
+{
+    m_next_view_index = i % m_tabs.size();
+    g_config.general.last_viewed_menu_index = i;
+}
+
+void MainMenuScene::HandleInput()
+{
+    bool nofocus = !ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow);
+    bool focus = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows |
+                                        ImGuiFocusedFlags_NoPopupHierarchy);
+
+    // XXX: Ensure we have focus for two frames. If a user cancels a popup window, we do not want to cancel main
+    //      window as well.
+    if (nofocus || (focus && m_had_focus_last_frame &&
+                    ImGui::IsNavInputTest(ImGuiNavInput_Cancel,
+                                          ImGuiInputReadMode_Pressed))) {
+        Hide();
+        return;
+    }
+
+    if (focus && m_had_focus_last_frame) {
+        if (ImGui::IsKeyPressed(ImGuiKey_GamepadL1)) {
+            SetNextViewIndex((m_current_view_index + m_tabs.size() - 1) %
+                             m_tabs.size());
+        }
+
+        if (ImGui::IsKeyPressed(ImGuiKey_GamepadR1)) {
+            SetNextViewIndex((m_current_view_index + 1) % m_tabs.size());
+        }
+    }
+
+    m_had_focus_last_frame = focus;
+}
+
+bool MainMenuScene::Draw()
+{
+    m_animation.Step();
+    m_background.Draw();
+    m_nav_control_view.Draw();
+
+    ImGuiIO &io = ImGui::GetIO();
+    float t = m_animation.GetSinInterpolatedValue();
+    float window_alpha = t;
+
+    ImGui::PushStyleVar(ImGuiStyleVar_Alpha, window_alpha);
+    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
+    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
+    ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0);
+    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
+
+    ImVec4 extents = g_viewport_mgr.GetExtents();
+    ImVec2 window_pos = ImVec2(io.DisplaySize.x / 2, extents.y);
+    ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, ImVec2(0.5, 0));
+
+    ImVec2 max_size = g_viewport_mgr.Scale(ImVec2(800, 0));
+    float x = fmin(io.DisplaySize.x - extents.x - extents.z, max_size.x);
+    float y = io.DisplaySize.y - extents.y - extents.w;
+    ImGui::SetNextWindowSize(ImVec2(x, y));
+
+    if (ImGui::Begin("###MainWindow", NULL,
+                     ImGuiWindowFlags_NoDecoration |
+                         ImGuiWindowFlags_NoSavedSettings)) {
+        //
+        // Nav menu
+        //
+
+        float width = ImGui::GetWindowWidth();
+        float nav_width = width * 0.3;
+        float content_width = width - nav_width;
+
+        ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(26,26,26,255));
+
+        ImGui::BeginChild("###MainWindowNav", ImVec2(nav_width, -1), true, ImGuiWindowFlags_NavFlattened);
+
+        bool move_focus_to_tab = false;
+        if (m_current_view_index != m_next_view_index) {
+            m_current_view_index = m_next_view_index;
+            if (!m_focus_view) {
+                move_focus_to_tab = true;
+            }
+        }
+
+        int i = 0;
+        for (auto &button : m_tabs) {
+            if (move_focus_to_tab && i == m_current_view_index) {
+                ImGui::SetKeyboardFocusHere();
+                move_focus_to_tab = false;
+            }
+            if (button->Draw(i == m_current_view_index)) {
+                SetNextViewIndex(i);
+            }
+            if (i == m_current_view_index) {
+                ImGui::SetItemDefaultFocus();
+            }
+            i++;
+        }
+        ImGui::EndChild();
+        ImGui::PopStyleColor();
+
+        //
+        // Content
+        //
+        ImGui::SameLine();
+        int s = ImGui::GetTextLineHeight() * 0.75;
+        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(s, s));
+        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(s, s));
+        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6*g_viewport_mgr.m_scale);
+
+        ImGui::PushID(m_current_view_index);
+        ImGui::BeginChild("###MainWindowContent", ImVec2(content_width, -1),
+                          true,
+                          ImGuiWindowFlags_AlwaysUseWindowPadding |
+                              ImGuiWindowFlags_NavFlattened);
+
+        if (!g_input_mgr.IsNavigatingWithController()) {
+            // Close button
+            ImGui::PushFont(g_font_mgr.m_menu_font);
+            ImGuiStyle &style = ImGui::GetStyle();
+            ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 128));
+            ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
+            ImVec2 pos = ImGui::GetCursorPos();
+            ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - style.FramePadding.x * 2.0f - ImGui::GetTextLineHeight());
+            if (ImGui::Button(ICON_FA_XMARK)) {
+                Hide();
+            }
+            ImGui::SetCursorPos(pos);
+            ImGui::PopStyleColor(2);
+            ImGui::PopFont();
+        }
+
+        ImGui::PushFont(g_font_mgr.m_default_font);
+        if (m_focus_view) {
+            ImGui::SetKeyboardFocusHere();
+            m_focus_view = false;
+        }
+        m_tabs[m_current_view_index]->view()->Draw();
+
+        ImGui::PopFont();
+        ImGui::EndChild();
+        ImGui::PopID();
+        ImGui::PopStyleVar(3);
+
+        HandleInput();
+    }
+    ImGui::End();
+    ImGui::PopStyleVar(5);
+
+    return !m_animation.IsComplete();
+}
+
+MainMenuScene g_main_menu;
diff --git a/ui/xui/main-menu.hh b/ui/xui/main-menu.hh
new file mode 100644
index 0000000000..5a0efd01f3
--- /dev/null
+++ b/ui/xui/main-menu.hh
@@ -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;
diff --git a/ui/xui/main.cc b/ui/xui/main.cc
new file mode 100644
index 0000000000..bdba750434
--- /dev/null
+++ b/ui/xui/main.cc
@@ -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;
+    }
+}
diff --git a/ui/xui/menubar.cc b/ui/xui/menubar.cc
new file mode 100644
index 0000000000..8b1564a44f
--- /dev/null
+++ b/ui/xui/menubar.cc
@@ -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();
+    }
+}
diff --git a/ui/xui/menubar.hh b/ui/xui/menubar.hh
new file mode 100644
index 0000000000..00774afc48
--- /dev/null
+++ b/ui/xui/menubar.hh
@@ -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();
diff --git a/ui/xui/meson.build b/ui/xui/meson.build
new file mode 100644
index 0000000000..088e68c764
--- /dev/null
+++ b/ui/xui/meson.build
@@ -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'))
diff --git a/ui/xui/misc.hh b/ui/xui/misc.hh
new file mode 100644
index 0000000000..b2c8a9e99d
--- /dev/null
+++ b/ui/xui/misc.hh
@@ -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;
+}
diff --git a/ui/xui/monitor.cc b/ui/xui/monitor.cc
new file mode 100644
index 0000000000..4566425330
--- /dev/null
+++ b/ui/xui/monitor.cc
@@ -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;
diff --git a/ui/xui/monitor.hh b/ui/xui/monitor.hh
new file mode 100644
index 0000000000..e37388a033
--- /dev/null
+++ b/ui/xui/monitor.hh
@@ -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;
diff --git a/ui/xui/notifications.cc b/ui/xui/notifications.cc
new file mode 100644
index 0000000000..42fbbf9657
--- /dev/null
+++ b/ui/xui/notifications.cc
@@ -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);
+}
diff --git a/ui/xui/notifications.hh b/ui/xui/notifications.hh
new file mode 100644
index 0000000000..77a97d5915
--- /dev/null
+++ b/ui/xui/notifications.hh
@@ -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;
diff --git a/ui/xui/popup-menu.cc b/ui/xui/popup-menu.cc
new file mode 100644
index 0000000000..6cad92e2a1
--- /dev/null
+++ b/ui/xui/popup-menu.cc
@@ -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;
diff --git a/ui/xui/popup-menu.hh b/ui/xui/popup-menu.hh
new file mode 100644
index 0000000000..f5556ca5e5
--- /dev/null
+++ b/ui/xui/popup-menu.hh
@@ -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;
diff --git a/ui/xemu-reporting.cc b/ui/xui/reporting.cc
similarity index 75%
rename from ui/xemu-reporting.cc
rename to ui/xui/reporting.cc
index 5b946d2f64..dc14647572 100644
--- a/ui/xemu-reporting.cc
+++ b/ui/xui/reporting.cc
@@ -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
diff --git a/ui/xemu-reporting.h b/ui/xui/reporting.hh
similarity index 52%
rename from ui/xemu-reporting.h
rename to ui/xui/reporting.hh
index 383bb2a615..f823bc4c3c 100644
--- a/ui/xemu-reporting.h
+++ b/ui/xui/reporting.hh
@@ -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
diff --git a/ui/xui/scene-components.cc b/ui/xui/scene-components.cc
new file mode 100644
index 0000000000..35e2434430
--- /dev/null
+++ b/ui/xui/scene-components.cc
@@ -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
diff --git a/ui/xui/scene-components.hh b/ui/xui/scene-components.hh
new file mode 100644
index 0000000000..d5b86e7c54
--- /dev/null
+++ b/ui/xui/scene-components.hh
@@ -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
diff --git a/ui/xui/scene-manager.cc b/ui/xui/scene-manager.cc
new file mode 100644
index 0000000000..ef938a0f8f
--- /dev/null
+++ b/ui/xui/scene-manager.cc
@@ -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;
+}
diff --git a/ui/xui/scene-manager.hh b/ui/xui/scene-manager.hh
new file mode 100644
index 0000000000..776c896caa
--- /dev/null
+++ b/ui/xui/scene-manager.hh
@@ -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;
diff --git a/ui/xui/scene.cc b/ui/xui/scene.cc
new file mode 100644
index 0000000000..357a54db2b
--- /dev/null
+++ b/ui/xui/scene.cc
@@ -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; }
diff --git a/ui/xui/scene.hh b/ui/xui/scene.hh
new file mode 100644
index 0000000000..665c76d321
--- /dev/null
+++ b/ui/xui/scene.hh
@@ -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();
+};
diff --git a/ui/xui/update.cc b/ui/xui/update.cc
new file mode 100644
index 0000000000..306ec7dfeb
--- /dev/null
+++ b/ui/xui/update.cc
@@ -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);
+}
diff --git a/ui/xui/update.hh b/ui/xui/update.hh
new file mode 100644
index 0000000000..f77336b04d
--- /dev/null
+++ b/ui/xui/update.hh
@@ -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
diff --git a/ui/xui/viewport-manager.cc b/ui/xui/viewport-manager.cc
new file mode 100644
index 0000000000..c45d7a8be4
--- /dev/null
+++ b/ui/xui/viewport-manager.cc
@@ -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);
+    }
+}
diff --git a/ui/xui/viewport-manager.hh b/ui/xui/viewport-manager.hh
new file mode 100644
index 0000000000..ca98e6163f
--- /dev/null
+++ b/ui/xui/viewport-manager.hh
@@ -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;
diff --git a/ui/xui/welcome.cc b/ui/xui/welcome.cc
new file mode 100644
index 0000000000..837fe04af9
--- /dev/null
+++ b/ui/xui/welcome.cc
@@ -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;
diff --git a/ui/xui/welcome.hh b/ui/xui/welcome.hh
new file mode 100644
index 0000000000..9c99613c86
--- /dev/null
+++ b/ui/xui/welcome.hh
@@ -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;
diff --git a/ui/xui/widgets.cc b/ui/xui/widgets.cc
new file mode 100644
index 0000000000..15d6565111
--- /dev/null
+++ b/ui/xui/widgets.cc
@@ -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();
+    }
+}
diff --git a/ui/xui/widgets.hh b/ui/xui/widgets.hh
new file mode 100644
index 0000000000..5ee99be815
--- /dev/null
+++ b/ui/xui/widgets.hh
@@ -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);
diff --git a/ui/xemu-hud.h b/ui/xui/xemu-hud.h
similarity index 95%
rename from ui/xemu-hud.h
rename to ui/xui/xemu-hud.h
index e9a50bfbb6..a510c85bf6 100644
--- a/ui/xemu-hud.h
+++ b/ui/xui/xemu-hud.h
@@ -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
 }