diff --git a/client/devkit_client/gui2/SDL2_image.dll b/client/devkit_client/gui2/SDL2_image.dll
new file mode 100644
index 0000000000000000000000000000000000000000..d90de2a3014fe703b7bc954a67fc9cc81c972bf6
Binary files /dev/null and b/client/devkit_client/gui2/SDL2_image.dll differ
diff --git a/client/devkit_client/gui2/gui2.py b/client/devkit_client/gui2/gui2.py
index 13343ebd20c76220018c4e180358079bce2bd927..860e8e028cfeb5ca830ca588c96999a934ad6803 100644
--- a/client/devkit_client/gui2/gui2.py
+++ b/client/devkit_client/gui2/gui2.py
@@ -35,6 +35,8 @@ import signalslot
 logging.getLogger('OpenGL.plugins').setLevel(logging.ERROR)
 import OpenGL.GL as gl
 import sdl2
+# requires PySDL2-dll
+import sdl2.sdlimage
 import imgui
 import imgui.integrations.sdl2
 
@@ -53,6 +55,8 @@ GUEST_LAN_PATTERN = 'DISABLE'
 
 TOGGLE_DEV_MODE = 'If this is not a network issue, please toggle developer mode off then back on for the device and try again.'
 
+ICON_FILENAME = 'logo-steamdeck-256.tga'
+
 logger = logging.getLogger(__name__)
 
 
@@ -3079,6 +3083,75 @@ class ImGui_SDL2_Viewport:
             logger.warning('SDL_CreateWindow failed: {}'.format(sdl2.SDL_GetError()))
             return False
 
+        icon_file = os.path.join(devkit_client.ROOT_DIR, ICON_FILENAME).encode()
+        assert os.path.exists(icon_file)
+
+        # https://github.com/libsdl-org/SDL/blob/SDL2/src/video/x11/SDL_x11window.c#L750
+        # SDL_assert(icon->format->format == SDL_PIXELFORMAT_ARGB8888);
+        # https://github.com/libsdl-org/SDL/blob/SDL2/src/video/windows/SDL_windowswindow.c#L639
+        # ok, not documented otherwise, but SDL_SetWindowIcon expects ARGB8888
+
+        icon_image = sdl2.sdlimage.IMG_Load(icon_file)
+        assert icon_image.contents.format.contents.format == sdl2.SDL_PIXELFORMAT_ARGB8888
+        # this works, but requires PySDL2-dll and SDL_Image, and I didn't want it
+        # NARRATOR: he later learned that was a mistake
+#        sdl2.SDL_SetWindowIcon(self.sdl_window, icon_image)
+#        sdl2.SDL_FreeSurface(icon_image)
+
+        # Let's direct load instead!
+        # Uncompressed TGAs are trivially easy, we don't need sdl2.sdlimage..
+        tga = open(icon_file,'rb').read()
+        assert tga[0] == 0 # no image ID
+        assert tga[1] == 0 # no colormap
+        assert tga[2] == 2 # uncompressed true-color image
+        w = int.from_bytes(tga[12:12+2], byteorder='little', signed=False)
+        h = int.from_bytes(tga[14:14+2], byteorder='little', signed=False)
+        depth = tga[16]
+        assert depth == 32 # RGBA
+
+        # It's a little odd to be giving the line stride in those APIs (w*4), but whatever..
+        # BUG (??): does not load any pixels .. you just get a blank icon
+        # tried variations with SDL_CreateRGBSurfaceFrom and other pixel formats .. none of them worked
+        tga_surface = sdl2.SDL_CreateRGBSurfaceWithFormatFrom(tga[18:18+w*h*4], w, h, depth, w*4, sdl2.SDL_PIXELFORMAT_ARGB8888)
+        assert tga_surface is not None
+        assert tga_surface.contents.format.contents.format == sdl2.SDL_PIXELFORMAT_ARGB8888
+
+        # Try to force a solid fill?
+        # BUG (??): that doesn't work either, whatever happened with the surface setup, we can't write to it
+        pixies = ctypes.cast(tga_surface.contents.pixels, ctypes.POINTER(ctypes.c_uint32))
+        i = 0
+        while i < w*h:
+            # ARGB, alpha is the high byte, solid green
+            pixies[i] = 0xff00ff00 # BGRA32
+            i += 1
+
+        # ^ go ahead, test the above:
+        #sdl2.SDL_SetWindowIcon(self.sdl_window, tga_surface)
+        #sdl2.SDL_FreeSurface(tga_surface)
+
+        # Let's create a surface from scratch and see if that works better
+        # order is red, green, blue, alpha masks. swizzle away
+        tga_surface = sdl2.SDL_CreateRGBSurface(0, w, h, depth, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000)
+        assert tga_surface.contents.format.contents.format == sdl2.SDL_PIXELFORMAT_ARGB8888
+
+        # solid fill
+        pixies = ctypes.cast(tga_surface.contents.pixels, ctypes.POINTER(ctypes.c_uint32))
+        i = 0
+        while i < w*h:
+            # ARGB .. alpha is the high byte, solid green
+            pixies[i] = 0xff00ff00
+            i += 1
+
+        # smash the image pixels in as is .. victory!
+        pixies = ctypes.cast(tga_surface.contents.pixels, ctypes.POINTER(ctypes.c_byte))
+        i = 0
+        while i < w*h*4:
+            pixies[i] = tga[18+i]
+            i += 1
+
+        sdl2.SDL_SetWindowIcon(self.sdl_window, tga_surface)
+        sdl2.SDL_FreeSurface(tga_surface)
+
         self.gl_context = sdl2.SDL_GL_CreateContext(self.sdl_window)
         if self.gl_context is None:
             logger.warning('SDL_GL_CreateContext failed: {}'.format(sdl2.SDL_GetError()))
diff --git a/client/logo-steamdeck-256.tga b/client/logo-steamdeck-256.tga
new file mode 100644
index 0000000000000000000000000000000000000000..6a7fcfa0c7e21ee84a71c2a90645776da6a0c47d
Binary files /dev/null and b/client/logo-steamdeck-256.tga differ
diff --git a/requirements.txt b/requirements.txt
index 1ba50b796f1226eafe109f264dd2f6106d1d22b9..6d18e28e1e213282a01974d35af303d001c50413 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-PySDL2
+PySDL2-dll
 PyOpenGL
 signalslot >= 0.2.0
 ifaddr
diff --git a/setup/package-linux.py b/setup/package-linux.py
index 76db817992df8c2f6b7dc1004c0dc5de253d60c2..714e2648003edba88e002a2e5705001560560b7f 100755
--- a/setup/package-linux.py
+++ b/setup/package-linux.py
@@ -17,6 +17,10 @@ SETUP_DIR = os.path.abspath(os.path.dirname(__file__))
 ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 CLIENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../client'))
 
+# cursed, but delicious
+sys.path.append(CLIENT_DIR)
+from devkit_client.gui2.gui2 import ICON_FILENAME
+sys.path.pop()
 
 # don't let python buffering get in the way or readable output
 # https://stackoverflow.com/questions/107705/disable-output-buffering
@@ -80,6 +84,7 @@ if __name__ == '__main__':
                 os.path.join(zipappdirname, 'site-packages/devkit-utils')
                 )
             shutil.copy(os.path.join(ROOT_DIR, 'ChangeLog'), zipappdirname)
+            shutil.copy(os.path.join(ROOT_DIR, 'client', ICON_FILENAME), os.path.join(zipappdirname, 'site-packages'))
             output_path = os.path.abspath(os.path.join(conf.output_directory, output_name))
             zipapp.create_archive(
                 zipappdirname,
diff --git a/setup/package-windows.py b/setup/package-windows.py
index 348527f27fa4dffacdc2d2383c5e452cb1d730c1..d11c52ed653d41032eadb5fcf17a102c83cca0f9 100644
--- a/setup/package-windows.py
+++ b/setup/package-windows.py
@@ -19,6 +19,7 @@ DIST_DIR = os.path.join(ROOT_DIR, 'dist')
 # cursed, but delicious
 sys.path.append(CLIENT_DIR)
 from devkit_client import locate_cygwin_tools
+from devkit_client.gui2.gui2 import ICON_FILENAME
 sys.path.pop()
 
 # don't let python buffering get in the way or readable output
@@ -93,6 +94,7 @@ if __name__ == '__main__':
     shutil.copytree(os.path.join(ROOT_DIR, 'third-party-licenses'), os.path.join(DIST_DIR, 'third-party-licenses'), dirs_exist_ok=True)
 
     shutil.copy(os.path.join(ROOT_DIR, 'ChangeLog'), DIST_DIR)
+    shutil.copy(os.path.join(CLIENT_DIR, ICON_FILENAME), DIST_DIR)
 
     zippath = os.path.join(ROOT_DIR, 'devkit-gui-win64.zip')
     print(f'Creating archive: {zippath}')