Skip to content

Linux port: Vulkan/X11/Wayland rendering backend#525

Open
alansley wants to merge 28 commits intoe-dream-ai:masterfrom
alansley:linux-adjustments
Open

Linux port: Vulkan/X11/Wayland rendering backend#525
alansley wants to merge 28 commits intoe-dream-ai:masterfrom
alansley:linux-adjustments

Conversation

@alansley
Copy link
Copy Markdown

Problem

When Metal replaced the OpenGL renderer on macOS, RendererGL.cpp was never replaced for Linux. The Linux build had no rendering backend, no CMake build system, and accumulated compilation errors against modern toolchains (GCC, FFmpeg 6+, Boost 1.69+).

Approach

Add a Vulkan backend for Linux with native Wayland support. Platform-specific code is isolated behind #ifdef LINUX_GNU / #ifdef HAVE_WAYLAND guards; Mac paths are unchanged.

What's included

Cross-platform fixes (safe on all platforms):

  • SmartPtr.h — guard _LIBCPP_INLINE_VISIBILITY for libstdc++
  • BlockingQueue.h — fix condition_variable::wait() to take a lock object, not a raw mutex
  • XTimer.h — remove <X11/Xlib.h> (its None macro clashes with enum class TransitionType); make Time() const
  • FrameDisplay.h — guard <CoreVideo/CVPixelBuffer.h> inside #ifdef MAC
  • StringFormat.cpp — add missing <cstdarg>
  • client.h — wrap SHAREDIR in std::string() so it stores a path string in JSON, not a boolean
  • ContentDecoder.cpp — replace removed avcodec_close with avcodec_free_context (FFmpeg 6+)
  • PlaylistManager.h — add missing <condition_variable> / <mutex>
  • TextureFlat.h — wrap FFmpeg headers in extern "C"
  • EDreamClient.h — add missing SetQuota() declaration
  • EDreamClient.cpp — fix string_view passed to %s (UB on GCC, crashes at runtime)

Linux platform additions:

  • LinuxBuild/CMakeLists.txt — finds Vulkan, X11, Boost, FFmpeg, OpenSSL, curl, libpng; compiles GLSL→SPIR-V via glslc
  • PlatformUtils_Linux.cpp/proc/self/exe, xdg-open, pthread naming, X11 blank cursor, OpenSSL MD5
  • cpu_usage_linux.h/proc/stat replaces glibtop; GetNumCores() via sysconf
  • DisplayVulkan — X11 window + VK_KHR_xlib_surface; native Wayland via VK_KHR_wayland_surface + xdg-shell when WAYLAND_DISPLAY is set; XScreensaver embed always uses X11; FIFO present mode for vsync
  • RendererVulkan — graphics pipeline, per-frame acquire/present, swapchain recreation on VK_ERROR_OUT_OF_DATE_KHR / VK_SUBOPTIMAL_KHR (required on Wayland for initial surface configuration)
  • TextureFlatVulkanVkImage upload from CImage or decoded AVFrame (RGBA fast-path, dirty flag for re-bind after upload)
  • shaders/quad.{vert,frag} — fullscreen blit with correct normalised coordinate handling
  • socket.io-client-cpp/lib/linux/libsioclient{,_tls}.a — Linux ELF builds (Mach-O binaries untouched)
  • API key auth via INFINIDREAM_API_KEY env var or stored setting, for headless/Linux environments without a browser-based sign-in flow

Verified working

cd client_generic/LinuxBuild && cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build && ./build/infinidream

Arch Linux · kernel 6.19 · RTX 4090 · Vulkan 1.4.341 · GCC 15.1 · Boost 1.89 · FFmpeg 7.x

  • X11/XWayland, native Wayland, and XScreensaver embed paths all functional
  • Content downloads and video plays back correctly

Mac safety

All changes to shared files add new #elif/#else branches or fix genuine correctness bugs. Existing #ifdef MAC blocks are byte-for-byte identical to upstream. The Mach-O socket.io binaries are not modified.

amillionbouncyballs and others added 15 commits March 22, 2026 17:08
Ten standalone bug fixes that are correct on all platforms. Each was a
latent bug on macOS masked by libc++/GLUT/Metal; they surface as hard
errors on Linux/libstdc++/Vulkan.

- Common/SmartPtr.h: guard _LIBCPP_INLINE_VISIBILITY — libstdc++ does
  not define _LIBCPP_HIDE_FROM_ABI, so the macro expansion failed to
  compile on GCC.

- Common/BlockingQueue.h: condition_variable::wait() requires a lock
  object, not a raw mutex. Three call sites passed m_mutex directly
  (undefined behaviour). Also mark condition var members mutable so
  const methods can legally call wait().

- Common/XTimer.h: remove unused <X11/Xlib.h> / <X11/Xutil.h> —
  these headers define the None macro (0L) which clashes with
  enum class TransitionType { None }. Also make Time() const to
  satisfy the ITimer pure-virtual signature.

- Client/FrameDisplay.h: guard <CoreVideo/CVPixelBuffer.h> inside
  #ifdef MAC — CoreVideo does not exist on Linux.

- Client/StringFormat.cpp: add missing #include <cstdarg> for
  va_list / va_start / va_end.

- Client/client.h: wrap SHAREDIR in std::string() when calling
  Set("settings.app.InstallDir", ...). SHAREDIR is const char*; without
  the cast, overload resolution picks Set(string_view, bool) (non-null
  pointer → true) instead of Set(string_view, string_view), storing a
  boolean in JSON and crashing later at as_string().

- ContentDecoder/ContentDecoder.cpp: avcodec_close() was removed in
  FFmpeg 6.x. Replace with avcodec_free_context() which also nulls the
  pointer.

- ContentDownloader/PlaylistManager.h: add missing <condition_variable>
  and <mutex> for the std::condition_variable / std::mutex members.

- DisplayOutput/Renderer/TextureFlat.h: wrap FFmpeg headers in
  extern "C". Without it, sws_getContext gets C++ name mangling and
  fails to link even with -lswscale present.

- Networking/EDreamClient.h: add missing SetQuota() declaration.
  The method is defined in EDreamClient.cpp and called from a free
  function, but was absent from the header, causing a compile error
  on strict-conformance builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Modify existing files to route Linux through Vulkan instead of falling
through to Metal (which only exists on macOS). No Mac code paths are
changed — all additions are inside new #elif defined(LINUX_GNU) or
#else branches that were previously absent or broken.

- Client/Player.cpp: change bare #else after WIN32 guard to
  #elif defined(MAC) for Metal includes, add new #else block that
  includes DisplayVulkan.h / RendererVulkan.h. AddDisplay() gains a
  matching Linux branch that constructs CDisplayVulkan + CRendererVulkan.
  Remove dead EndFrameUpdate() FPS-capping code (spFD / m_PlayerFps
  undefined on Linux; the block was never reachable on Mac either due
  to the surrounding #ifndef USE_METAL guard).

- Client/main.cpp: remove <GL/gl.h> / <GL/glut.h> includes from the
  Linux branch (Vulkan does not use GLUT). Guard glutInit() inside
  #if defined(MAC) so it is not called on Linux.

- Client/client_linux.h: remove #include "lua_playlist.h" (file does
  not exist). Add #include "PlatformUtils.h". Fix m_AppData path to
  ~/.config/infinidream/. Derive m_WorkingDir from
  PlatformUtils::GetWorkingDir() rather than the autotools SHAREDIR
  macro. Fix g_Player().AddDisplay() missing nullptr second argument.
  Remove g_Player().Framerate() call (method no longer exists). Fix
  CElectricSheep::Update() call to pass required int argument. Replace
  invalid static_cast<spCKeyEvent> with std::static_pointer_cast.
  Remove entire stale voting block (m_pVoter, GetCurrentPlayingID,
  RepeatSheep, m_spSplashPos/Neg — all removed from current codebase).

- Client/cpu_usage_linux.h: replace glibtop (GNOME library, requires
  libgtop2 which is not a standard dependency) with direct /proc/stat
  parsing. ReadProcStat() uses fscanf with a format string that matches
  the literal "cpu" prefix — no throwaway variables, no string
  allocation, returns matched == 4 on success. GetNumCores() uses
  sysconf(_SC_NPROCESSORS_ONLN) instead of parsing /proc/cpuinfo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Everything needed to configure and compile the project on Linux.
No existing Mac build files are touched.

LinuxBuild/CMakeLists.txt
  Standalone CMake build for Linux (mirrors MacBuild structure).
  Targets: Vulkan, X11, Boost (filesystem/thread/log/log_setup/json),
  FFmpeg (via pkg-config), OpenSSL, libcurl, libpng, pthreads.
  Compiles GLSL shaders to SPIR-V via glslc (from Vulkan SDK or PATH)
  and copies them next to the binary at POST_BUILD. Defines LINUX_GNU,
  BOOST_FILESYSTEM_VERSION=3, SHAREDIR=".".

LinuxBuild/PlatformUtils_Linux.cpp
  Implements the PlatformUtils static interface for Linux:
  IsInternetReachable (connect to 8.8.8.8:53), GetWorkingDir / GetAppPath
  (/proc/self/exe), OpenURLExternally (xdg-open), SetThreadName
  (pthread_setname_np), SetCursorHidden (X11 blank cursor),
  SetOnMouseMovedCallback, DispatchOnMainThread (thread-safe queue
  drained on the main thread), NotifyError (stderr), CalculateFileMD5
  (OpenSSL EVP — same logic as the Mac version).

LinuxBuild/PlatformUtils_Internal.h
  Internal helpers shared between DisplayVulkan and PlatformUtils_Linux:
  DrainMainThreadQueue(), GetMouseCallback(), GetCursorHidden().

socket.io-client-cpp/lib/linux/libsioclient{,_tls}.a
  Linux ELF builds of socket.io-client-cpp. The vendored .a files in
  lib/ are Mach-O universal binaries used by the Xcode project and are
  left untouched. Linux links against the lib/linux/ variants via CMake.
  To rebuild: clone socket.io-client-cpp, cmake -DUSE_TLS=1, make.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New DisplayOutput/Vulkan/ directory implements the CDisplayOutput and
CRenderer interfaces for Linux using Vulkan + X11. The Metal backend
(DisplayOutput/Metal/) is untouched.

DisplayVulkan (.h/.cpp)
  Creates an X11 Window and a VkSurfaceKHR via VK_KHR_xlib_surface.
  Manages VkInstance, VkDevice (picks first discrete GPU, falls back to
  integrated), VkSwapchainKHR, render pass, framebuffers, and per-frame
  sync primitives (semaphores + fence). Fullscreen via EWMH
  _NET_WM_STATE_FULLSCREEN. Handles ConfigureNotify resize by
  recreating the swapchain. XScreensaver mode: if XSCREENSAVER_WINDOW
  env var is set the window is reparented into it rather than created
  standalone. Implements CDisplayOutput: Initialize, Resize, SetTitle,
  SetFullScreen, Present, SwapBuffers, ProcessEvents.

RendererVulkan (.h/.cpp)
  Implements CRenderer. Allocates a VkDescriptorPool, descriptor set
  layout, and a graphics pipeline backed by the compiled quad shaders.
  Per-frame: acquires swapchain image, begins render pass, binds
  pipeline + descriptor set, draws the fullscreen quad, ends pass,
  submits, presents. BeginFrame/EndFrame bracket each rendered frame.
  NewTextureFlat() returns a CTextureFlatVulkan.

TextureFlatVulkan (.h/.cpp)
  Implements CTextureFlat. Allocates a VkImage + VkImageView + VkSampler
  and a host-visible VkDeviceMemory staging buffer. Upload(spCImage)
  copies RGBA pixel data via vkMapMemory then transitions the image to
  SHADER_READ_ONLY_OPTIMAL. BindFrame(spCVideoFrame) maps decoded AVFrame
  data directly into the staging buffer each frame for zero-copy video
  upload. The descriptor set is updated with vkUpdateDescriptorSets so
  the fragment shader samples the current frame.

ShaderVulkan (.h/.cpp)
  Loads SPIR-V bytecode (compiled from shaders/quad.vert / quad.frag at
  build time by glslc). Creates VkShaderModule.

FontVulkan (.h/.cpp)
  Stub implementing CFont. Text rendering is not yet wired up on Linux;
  the stub satisfies the interface so the binary links and runs.

shaders/quad.vert
  Fullscreen triangle-pair. Passes UV coordinates to the fragment stage.

shaders/quad.frag
  Samples a combined image sampler (the current video frame texture)
  and writes to the swapchain image. YUV→RGB conversion is handled
  upstream by FFmpeg swscale (frames arrive as RGBA); the shader is a
  straight passthrough blit.

Verified on Arch Linux, RTX 4090, Vulkan 1.4.341, driver 575.57.08:
  cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build
  ./build/infinidream   →  window opens, content decoder and downloader
                            initialise, video playback begins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
At runtime, if WAYLAND_DISPLAY is set and XSCREENSAVER_WINDOW is not,
DisplayVulkan connects natively to the Wayland compositor using
VK_KHR_wayland_surface instead of VK_KHR_xlib_surface. XScreensaver
embed mode always uses X11 (that protocol is inherently X11). On
X11-only systems or if wayland-client is absent at build time, behaviour
is identical to before.

CMakeLists.txt:
  Optionally find wayland-client, wayland-protocols, and wayland-scanner
  via pkg-config. When all three are present, generates xdg-shell
  protocol bindings (xdg-shell-protocol.c / xdg-shell-client-protocol.h)
  at configure time via wayland-scanner, adds HAVE_WAYLAND compile
  definition, and links wayland-client. Graceful fallback to X11-only
  if any piece is missing.

DisplayVulkan.h / .cpp:
  createVulkanInstance() picks VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME or
  VK_KHR_XLIB_SURFACE_EXTENSION_NAME based on m_bWayland.

  initWayland() — connects to wl_display, binds wl_compositor and
  xdg_wm_base from the registry, creates wl_surface, xdg_surface, and
  xdg_toplevel, handles ping/configure/close via static listener
  callbacks (onXdgWmBasePing, onXdgSurfaceConfigure,
  onXdgToplevelConfigure, onXdgToplevelClose), sets fullscreen if
  requested, and does two wl_display_roundtrips to complete the initial
  configure handshake before returning.

  checkEvents() dispatches Wayland events with wl_display_flush() +
  wl_display_dispatch_pending() instead of XPending/XNextEvent.

  setFullScreen() calls xdg_toplevel_set_fullscreen / _unset_fullscreen
  on Wayland; EWMH ClientMessage on X11.

  Title() calls xdg_toplevel_set_title on Wayland; XStoreName on X11.

  destroyWayland() tears down xdg_toplevel → xdg_surface → wl_surface
  → xdg_wm_base → wl_compositor → wl_display in dependency order,
  after Vulkan surface/instance are already destroyed.

client_linux.h:
  Replace CElectricSheep::Update(0) (now requires two boost::barrier
  refs) with the equivalent direct calls: BeginFrameUpdate(),
  DoRealFrameUpdate(0), EndFrameUpdate().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EDreamClient::ParsePlaylist line 1631 passed a std::string_view to a
%s variadic argument. On Apple Clang/libc++ this accidentally works
because the ABI passes the pointer field first and it happens to be
null-terminated; on GCC/libstdc++ vsnprintf reads the struct bytes
differently and segfaults.

Fix: uuid.empty() ? "default playlist" : uuid.data()

uuid.data() is safe here — callers always pass a std::string-backed
view so the data pointer is null-terminated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Frame.h: set m_pFrame->format/width/height in first constructor (was
  left as AV_PIX_FMT_NONE=-1, causing sws_getContext to crash)
- Clip.cpp: use AV_PIX_FMT_RGBA on Linux so ContentDecoder pre-converts
  decoded frames; call BindFrame via USE_HW_ACCELERATION||LINUX_GNU guard
- TextureFlatVulkan: add Bind() override to set descriptor set on renderer;
  add dirty flag so Apply() re-binds after each BindFrame upload; fast-path
  RGBA upload without redundant swscale pass
- RendererVulkan: Apply() was a no-op stub — delegate to base class so
  texture Bind()/Dirty() logic actually runs
- quad.vert: input coords are normalized [0,1], not pixels — remove
  division by screenWidth/Height that squashed quad to a 1px strip
- websocketpp/constants.hpp: bump max_header_size 16KB→64KB for Heroku
  large set-cookie response headers
- libsioclient_tls.a: rebuild with patched websocketpp header
- client.h: fix Run() loop and add virtual Update() base
- EDreamClient.cpp: fix string_view passed to %s (UB on GCC)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ipts

The Mac build (primary platform) uses Xcode/CMake and is unaffected.
These files were remnants of the original Electric Sheep Linux autotools era:

- electricsheep-saver, electricsheep-saver-gnome: wrapper scripts pointing
  at an 'electricsheep' binary that no longer exists. Linux xscreensaver
  integration already works via the XSCREENSAVER_WINDOW env var in
  DisplayVulkan — no wrapper script needed.
- Makefile.am (root, Client/, MSVC/SettingsGUI/), configure.ac, Makefile.in:
  dead autotools scaffolding superseded by LinuxBuild/CMakeLists.txt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two targeted fixes for the high CPU usage observed on Linux (~57% CPU
for simple video playback).

RendererVulkan: switch swapchain from MAILBOX to FIFO present mode.
MAILBOX never blocks — vkQueuePresentKHR returns immediately and the
render loop spins at uncapped speed consuming a full CPU core. FIFO
is guaranteed supported by the Vulkan spec and blocks on each present
until the next vblank, giving vsync-throttled output for free with
zero extra code. Mac is unaffected (Vulkan backend is Linux-only).

client.h / Player.h: call FpsCap(m_PerceptualFPS) in the Run() loop
after each frame. FpsCap() already existed in Player.cpp (complete
with its own wall-clock timer) but was unreachable — it was buried
inside a WIN32-only private section. Move it to public and call it
from Run(), which caps frame delivery to the configured perceptual
FPS (default 20 fps) and sleeps away any remaining frame budget.
Run() is only entered from main.cpp on Linux; the Mac Cocoa/display-
link loop never calls it, so Mac behaviour is unchanged.

Together these changes eliminate the busy-spin that was burning ~30-40%
of a CPU core between frames and the uncapped GPU submission loop on
top.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The existing auth flow only knew how to refresh a WorkOS sealed session
(wos-session cookie). On Linux there is no UI to obtain one, so a fresh
install could never authenticate.

The backend middleware already accepts an Api-Key <key> header as an
alternative to the session cookie on all protected endpoints, so no
server changes are needed.

Changes:

EDreamClient: AppendAuthHeader()
  Centralises all request auth in one place. Previously every call site
  duplicated three lines of get-setting / build-header / append-header.
  Now: sealed session present → Cookie: wos-session=<session> (existing
  behaviour); no session but api_key present → Api-Key <key>; neither →
  no auth header (request will 401, existing error handling applies).
  All ~15 call sites replaced with AppendAuthHeader(spDownload).

EDreamClient: SignInWithApiKey()
  Persists the API key to settings.content.api_key and returns true.
  No round-trip to the server needed — the key is used directly on each
  subsequent request.

EDreamClient: Authenticate() fallback
  After sealed-session refresh fails, checks the INFINIDREAM_API_KEY
  environment variable. If set, calls SignInWithApiKey() and persists
  the key for future runs. Falls back to a key already stored in
  settings. This means Linux users can authenticate with:
    export INFINIDREAM_API_KEY=<key>  ./infinidream
  or by adding settings.content.api_key to ~/.electricsheep/settings.json.

WebSocket connection
  ConnectRemoteControlSocket() now passes Api-Key in the query map when
  no sealed session is available, matching the HTTP auth fallback.

Mac is unaffected: EDreamClient is shared but the Mac flow always
obtains a sealed session via the first-time setup UI before reaching
any of these paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The streaming-load lambda (detached thread that fetches a download link
and starts a clip) declared a bool return type via an early 'return false'
on the no-display-units error path, but fell off the end without returning
on the success path. Add 'return true' after the clip-start block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three related issues with the startup/loading screen on Linux:

client.h: startup screen invisible during buffering
  The render block was gated on (drawNoSheepIntro || drawn). When a clip
  is in buffering mode g_Player().Update() returns false (drawn=false),
  and if the user is logged in HasStarted() is true so drawNoSheepIntro
  is also false. Result: nothing rendered at all — pure black screen while
  waiting for the first frames to decode. Fix: hoist the startup-screen
  visibility check into a 'showStartup' bool and include it in the outer
  gate condition so the startup screen renders independently of whether
  the player has a frame ready to draw.

StartupScreen.h: white square when logo.png not found
  CImage::Create() initialises the 256×256 buffer to white. If Load()
  fails (file not found) the blank white image was still uploaded as a
  texture and rendered as a white square in the centre of the screen.
  Fix: check the return value of Load() and set m_spImageRef = nullptr on
  failure. The existing if (m_spImageRef) guard around the DrawQuad then
  cleanly skips the logo without affecting the text rendering below it.

CMakeLists.txt: deploy logo.png next to the binary at build time
  The startup screen looks for logo.png relative to the working directory
  (settings.app.InstallDir). On Linux this is the build output directory,
  but the asset was never copied there. Add a POST_BUILD custom command
  that copies Runtime/logo.png next to the infinidream binary so it is
  found on first launch without manual installation steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HasStarted() returns true as soon as the playlist is set in the startup
thread, which happens before the first render frame. This meant
drawNoSheepIntro=false and m_StartupScreen was never created, so
showStartup=false and drawn=false → pure black screen.

Fix: include !drawn in the showStartup condition so the startup screen
is shown whenever there is no video frame available yet (buffering,
quota exhausted, no content downloaded, etc.) regardless of whether
HasStarted() returned true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Wayland, vkAcquireNextImageKHR returns VK_ERROR_OUT_OF_DATE_KHR
after the first few frames during initial surface configuration.
Previously BeginFrame() returned false without recreating the swapchain,
causing BeginDisplayFrame() to return false every subsequent frame.
Since Player::Update() is gated on BeginDisplayFrame(), it was never
called, so Clip::Update() never ran, the buffering threshold was never
checked, the frame queue filled to max (25 frames), and the decoder
thread blocked on push() — meaning video never played.

Fix: store the VkSurfaceKHR in Initialize(), implement recreateSwapchain()
(wait idle → destroy framebuffers/imageviews/swapchain → recreate), and
call it from both BeginFrame() and EndFrame() on OUT_OF_DATE or SUBOPTIMAL.

Also:
- Return bool from RenderFrame() so Update() can report whether it drew
- Fix SHAREDIR path suffix (\"./\" instead of \".\") in CMakeLists.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alansley
Copy link
Copy Markdown
Author

Example Linux output:
https://drive.google.com/file/d/1x4523AH8v7OzftZP8mQf62_KKWcYS7g-/view?usp=sharing

The frame-rate hitches / hiccups in the video are from OBS - they didn't occur live and everything was smooth.

amillionbouncyballs and others added 2 commits March 23, 2026 19:40
…isc fixes

Fullscreen / windowed toggle (Wayland):
- Add F key binding to toggle fullscreen (replaces Ctrl+F) in client_linux.h
- Add key-release guard (m_bPressed check) to prevent double-fire on toggle actions
- Add ToggleFullscreen() virtual to DisplayOutput base; override in CDisplayVulkan
- Set m_bFullScreen = bFullscreen in initWayland before the initial roundtrip so
  the configure-event filter correctly accepts the startup fullscreen configure
- Parse XDG_TOPLEVEL_STATE_FULLSCREEN from the wl_array in onXdgToplevelConfigure
  and ignore stale fullscreen configures when we have already requested windowed mode
- Set m_Width/m_Height = 1280x720 in setFullScreen(false) before unset_fullscreen
  so RendererVulkan recreates the swapchain at the correct windowed size
- Re-assert ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE on windowed transition
- Add proactive swapchain recreation in BeginFrame when display dimensions change
  (Wayland compositors do not always send VK_ERROR_OUT_OF_DATE_KHR for client-driven
  resizes)
- Add xdg-decoration-unstable-v1 protocol support (server-side window borders);
  falls back gracefully when the compositor does not advertise the interface (GNOME)
- Log all Wayland registry globals at startup for diagnostics
- Update CMakeLists.txt to generate xdg-decoration protocol bindings via
  wayland-scanner alongside the existing xdg-shell bindings

VAAPI / video decoder fixes:
- Fix VAAPI PTS loss: av_hwframe_transfer_data copies only pixel data; manually
  copy pts, pkt_dts, and best_effort_timestamp to sw_frame before av_frame_move_ref
- Guard against corrupted/wrapped last_played_frame resume values (e.g. uint32_t
  wrap from a previous VAAPI PTS bug) by clamping seekFrame to a sane range

Font / HUD rendering (FontVulkan):
- Implement FreeType-based glyph atlas for Vulkan (CFontVulkan)
- Wire up TextureFlatVulkan for font atlas texture upload

Startup / display:
- Fix startup screen visibility: show logo until the first video frame is ready
- Silence spurious PNG warnings for Mac-only OSD assets missing on Linux by
  removing the Warning log from the file-not-found path in LoadPNG.cpp

HUD / help text (client.h):
- Capitalise all F1 help text entries ("A: Slower playback" not "a: slower…")
- Show "F: Toggle full screen" on Linux instead of "Ctrl+F: Toggle full screen"

README: update Linux build and feature documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- client.h: make BK() macro a no-op on non-Linux builds; the \x01/\x02
  inline-bold markers are only handled by the Linux HUD renderer — on Mac
  they would appear as raw control characters in the F1 help overlay
- ContentDecoder.cpp: change #ifndef WIN32 guards to #ifdef LINUX_GNU so
  the VAAPI code (vaapi_get_format, hw device init, hw frame transfer)
  is only compiled on Linux; Mac retains its VideoToolbox path via the
  existing USE_HW_ACCELERATION / USE_METAL logic in base.h

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alansley
Copy link
Copy Markdown
Author

Text rendering from commit: 28d0d82

1.) Startup w/ partially faded out logo
Screenshot From 2026-03-23 19-20-42

2.) F1 - Keys / Help
Screenshot From 2026-03-23 19-21-20

3.) F2 - Status
Screenshot From 2026-03-23 19-22-32

Remove the FreeType-based CFontVulkan/CTextVulkan implementation and replace
it with ImGui-backed CFontImGui/CTextImGui, using ImGui's embedded ProggyForever
font (MIT licensed, no external font files required).

New build dependency: Dear ImGui added as a git submodule at
client_generic/imgui (pinned to v1.92.6, the latest stable release).
Its source files are compiled directly into the Linux build — no system
package or vcpkg entry needed.

- Drop FreeType dependency from LinuxBuild/CMakeLists.txt
- CFontImGui: registers font with ImGui atlas via AddFontDefaultVector()
- CTextImGui: GetExtent() measures via CalcTextSizeA()
- RendererVulkan: integrate ImGui frame lifecycle (NewFrame/Render/RenderDrawData)
- DrawText: segment-based renderer handles \n (newlines) and \t (tab stops at
  4×space width); \x01/\x02 bold markers are stripped — ProggyForever has no
  bold variant so bold text support is removed for now pending a future decision
  on font strategy
- Remove Lato-Regular.ttf from the build (was missing its license)
- Update README: remove freetype2 from prereqs, note ImGui submodule init

No Mac files were modified. The Metal renderer, Mac CMakeLists, OSD,
and CI workflow are all unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alansley
Copy link
Copy Markdown
Author

ImGui replacement of FreeType text rendering - no bold support so temporarily removed until there's a font strat:

1.) F1 Help
infinidream-linux-ImGui-F1-help

2.) F2 Stats
infinidream-linux-ImGui-F2-stats

@zquestz
Copy link
Copy Markdown

zquestz commented Mar 26, 2026

Build worked, but it doesn't launch on my Arch X11 system with integrated graphics (Intel).

❯ ./build/infinidream
[2026-03-26 12:22:45.601573] [0x00007f3aa0211240] [info]    Log Startup
CElectricSheep()
CElectricSheep_Linux()
Startup()
[2026-03-26 12:22:45.601699] [0x00007f3aa0211240] [info]    InitStorage()
[2026-Mar-26 12:22:45.601908 - info]: Attaching Log
[2026-Mar-26 12:22:45.602024 - info]: AttachLog()
[2026-Mar-26 12:22:45.602060 - info]: ******************* 0.0.0
[2026-Mar-26 12:22:45.602077 - info]: Working dir: /home/quest/src/nexus/infinidream/client_generic/LinuxBuild/build/
[2026-Mar-26 12:22:45.602114 - info]: Attempting to open Vulkan display...
[2026-Mar-26 12:22:45.621401 - info]: CDisplayVulkan: X11 initialized 1280x720 (fullscreen=1)
[2026-Mar-26 12:22:45.623311 - info]: CRendererVulkan: selected GPU: Intel(R) Arc(tm) Graphics (MTL)
infinidream: /home/quest/src/nexus/infinidream/client_generic/imgui/backends/imgui_impl_vulkan.cpp:1090: bool ImGui_ImplVulkan_CreateDeviceObjects(): Assertion `v->DescriptorPoolSize >= (8)' failed.
[1]    2549490 IOT instruction (core dumped)  ./build/infinidream

@alansley
Copy link
Copy Markdown
Author

Build worked, but it doesn't launch on my Arch X11 system with integrated graphics (Intel).

Thanks for the report - I'll take a look at it on the weekend.

@zquestz
Copy link
Copy Markdown

zquestz commented Mar 26, 2026

Got it working with this diff, it was AI generated, so I am sure there is a better way, but it might be useful for your real fix. =)

diff --git a/client_generic/DisplayOutput/Vulkan/DisplayVulkan.cpp b/client_generic/DisplayOutput/Vulkan/DisplayVulkan.cpp
index a83a2773..ae297387 100644
--- a/client_generic/DisplayOutput/Vulkan/DisplayVulkan.cpp
+++ b/client_generic/DisplayOutput/Vulkan/DisplayVulkan.cpp
@@ -494,6 +494,11 @@ bool CDisplayVulkan::Initialize(const uint32_t _width, const uint32_t _height,
         uint32_t winW = _bFullscreen ? m_WidthFS  : m_Width;
         uint32_t winH = _bFullscreen ? m_HeightFS : m_Height;
 
+        // Keep m_Width/m_Height in sync with the actual window size so the
+        // renderer's size-change check doesn't endlessly recreate the swapchain.
+        m_Width  = winW;
+        m_Height = winH;
+
         m_Window = XCreateSimpleWindow(
             m_pDisplay, RootWindow(m_pDisplay, screen),
             0, 0, winW, winH, 0,
diff --git a/client_generic/DisplayOutput/Vulkan/DisplayVulkan.h b/client_generic/DisplayOutput/Vulkan/DisplayVulkan.h
index 9a82000d..860edc23 100644
--- a/client_generic/DisplayOutput/Vulkan/DisplayVulkan.h
+++ b/client_generic/DisplayOutput/Vulkan/DisplayVulkan.h
@@ -122,6 +122,7 @@ class CDisplayVulkan : public CDisplayOutput
     VkSurfaceKHR GetSurface()   const { return m_surface; }
     Display*     GetXDisplay()  const { return m_pDisplay; }
     Window       GetXWindow()   const { return m_Window; }
+    bool         IsWayland()    const { return m_bWayland; }
 };
 
 } // namespace DisplayOutput
diff --git a/client_generic/DisplayOutput/Vulkan/RendererVulkan.cpp b/client_generic/DisplayOutput/Vulkan/RendererVulkan.cpp
index 083de4fc..9685a70d 100644
--- a/client_generic/DisplayOutput/Vulkan/RendererVulkan.cpp
+++ b/client_generic/DisplayOutput/Vulkan/RendererVulkan.cpp
@@ -833,7 +833,7 @@ bool CRendererVulkan::initImGui()
     initInfo.Device            = m_device;
     initInfo.QueueFamily       = m_graphicsFamily;
     initInfo.Queue             = m_graphicsQueue;
-    initInfo.DescriptorPoolSize = 2;  // imgui creates its own pool internally
+    initInfo.DescriptorPoolSize = IMGUI_IMPL_VULKAN_MINIMUM_IMAGE_SAMPLER_POOL_SIZE;  // imgui creates its own pool internally
     initInfo.MinImageCount     = 2;
     initInfo.ImageCount        = static_cast<uint32_t>(m_swapImages.size());
     // Since imgui 1.92 (Sept 2025): RenderPass and MSAASamples live in PipelineInfoMain.
@@ -998,13 +998,21 @@ bool CRendererVulkan::BeginFrame()
 {
     if (m_inFrame) return true;
 
-    // Proactively recreate the swapchain if the display size changed (e.g. fullscreen toggle).
+    // Proactively recreate the swapchain if the surface size changed (e.g. fullscreen toggle).
     // Wayland compositors don't always send VK_ERROR_OUT_OF_DATE_KHR for client-driven resizes.
-    if (m_spDisplay->Width()  != m_swapExtent.width ||
-        m_spDisplay->Height() != m_swapExtent.height)
+    // Query the actual Vulkan surface extent rather than the display's desired size, because
+    // X11 fullscreen transitions are asynchronous and the display dimensions may not yet
+    // match the real surface.
     {
-        recreateSwapchain();
-        return false;
+        VkSurfaceCapabilitiesKHR caps;
+        vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice, m_surface, &caps);
+        if (caps.currentExtent.width  != UINT32_MAX &&
+            (caps.currentExtent.width  != m_swapExtent.width ||
+             caps.currentExtent.height != m_swapExtent.height))
+        {
+            recreateSwapchain();
+            return false;
+        }
     }
 
     // Wait for previous frame in this slot
@@ -1016,11 +1024,14 @@ bool CRendererVulkan::BeginFrame()
         m_imageAvailable[m_currentFrame], VK_NULL_HANDLE,
         &m_currentImageIndex);
 
-    if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR)
+    if (result == VK_ERROR_OUT_OF_DATE_KHR)
     {
         recreateSwapchain();
         return false;  // Skip this frame
     }
+    // VK_SUBOPTIMAL_KHR: the image is still usable — proceed with rendering.
+    // Some X11/Wayland drivers (notably Intel Arc) return SUBOPTIMAL every
+    // frame, so recreating here would cause an infinite loop.
 
     vkResetFences(m_device, 1, &m_inFlightFence[m_currentFrame]);
 
@@ -1139,8 +1150,9 @@ bool CRendererVulkan::EndFrame(bool /*drawn*/)
     pi.pSwapchains        = &m_swapchain;
     pi.pImageIndices      = &m_currentImageIndex;
     VkResult presentResult = vkQueuePresentKHR(m_presentQueue, &pi);
-    if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || presentResult == VK_SUBOPTIMAL_KHR)
+    if (presentResult == VK_ERROR_OUT_OF_DATE_KHR)
         recreateSwapchain();
+    // VK_SUBOPTIMAL_KHR at present is harmless — the frame was shown.
 
     m_currentFrame = (m_currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
     m_inFrame      = false;
diff --git a/client_generic/Networking/Download.cpp b/client_generic/Networking/Download.cpp
index c80dc077..228bb18e 100644
--- a/client_generic/Networking/Download.cpp
+++ b/client_generic/Networking/Download.cpp
@@ -70,7 +70,7 @@ bool CFileDownloader::Perform(const std::string& _url)
         return false;
     if (!Verify(curl_easy_setopt(m_pCurl, CURLOPT_HEADERDATA, this)))
         return false;
-    
+
     return CCurlTransfer::Perform(_url);
 }
 
diff --git a/client_generic/Networking/EDreamClient.cpp b/client_generic/Networking/EDreamClient.cpp
index e4c420a5..e91f2601 100644
--- a/client_generic/Networking/EDreamClient.cpp
+++ b/client_generic/Networking/EDreamClient.cpp
@@ -616,14 +616,18 @@ bool EDreamClient::Authenticate()
 
     if (result == AuthRefreshResult::InvalidSession)
     {
-        g_Log->Warning("Auth refresh failed: session invalid or expired");
-        fIsLoggedIn.exchange(false);
-        g_Settings()->Set("settings.content.sealed_session", std::string(""));
-        g_Settings()->Storage()->Commit();
+        // The refresh endpoint may not support all session types (e.g. magic-link
+        // sessions).  Don't wipe the session — proceed as logged in and let
+        // subsequent API calls (Hello, playlist, etc.) determine whether the
+        // session is truly invalid.  If it is, those calls will return 401 and
+        // the HUD will show the "please sign in" message.
+        g_Log->Warning("Auth refresh failed, proceeding with existing session");
+        fIsLoggedIn.exchange(true);
         fInitialAuthComplete.store(true);
+        g_Player().SetOfflineMode(false);
         fAuthCV.notify_one();
-        // Do not auto-open login/settings UI on initial auth failure; HUD will show \"Please open settings to sign in.\"
-        return false;
+        boost::thread webSocketThread(&EDreamClient::ConnectRemoteControlSocket);
+        return true;
     }
 
     // TransientFailure: keep sealed session, notify so startup does not block, then retry with backoff
@@ -659,12 +663,13 @@ bool EDreamClient::Authenticate()
         }
         if (result == AuthRefreshResult::InvalidSession)
         {
-            g_Log->Warning("Auth refresh failed on retry: session invalid or expired");
+            // Don't wipe session — it may still be valid for API calls even if
+            // the refresh endpoint doesn't support this session type.
+            g_Log->Warning("Auth refresh failed on retry, proceeding with existing session");
             fAuthRetryPending.store(false);
-            g_Settings()->Set("settings.content.sealed_session", std::string(""));
-            g_Settings()->Storage()->Commit();
-            // Do not auto-open login/settings UI here; HUD will show \"Please open settings to sign in.\"
-            return false;
+            fIsLoggedIn.exchange(true);
+            DidSignIn();
+            return true;
         }
         // TransientFailure again: increase delay (3x, cap 24h)
         delaySeconds = std::min(delaySeconds * kRetryDelayMultiplier, kRetryDelayMaxSeconds);

amillionbouncyballs and others added 2 commits March 28, 2026 12:04
X11 fullscreen / window management:
- Create window at requested size and apply _NET_WM_STATE_FULLSCREEN as a
  window *property* before mapping (not a client message), so the WM
  resizes it to the correct monitor dimensions on startup.
- MapNotify wait loop now captures ConfigureNotify events so the initial
  fullscreen size is known before creating the Vulkan surface/swapchain.
- Add 500 ms fallback poll if ConfigureNotify arrives after MapNotify.
- Fix EWMH setFullScreen message: data.l[0] must be 0/1, not an Atom
  value; set data.l[2]=0, data.l[3]=1 (source: normal application).
- checkEvents(): update m_Width/m_Height on ConfigureNotify so HUD
  coordinates stay in sync with WM-driven window resizes at runtime.
- Add IsWayland() accessor to CDisplayVulkan.

Swapchain / HUD aspect ratio:
- createSwapchain(): use std::clamp(requested, min, max) instead of
  caps.currentExtent, which can lag behind ConfigureNotify on X11.
- BeginFrame(): trigger swapchain recreation when Display()->Width/Height()
  differs from m_swapExtent, replacing the surface-caps query that had
  the same lag problem.
- Set io.DisplaySize from Display()->Width/Height() so ImGui always
  reflects the true window dimensions.
- Remove max(1.0f, ...) floor from font scaling in DrawText, GetExtent,
  StatsConsole, and StartupScreen; use proportional screenH/1080 scaling.
- Compute startup logo aspect ratio dynamically each frame rather than
  baking it in the constructor, fixing logo squash after fullscreen toggle.

F1 help overlay:
- Remove inline bold markers (\x01/\x02) from BK macro — ProggyForever
  does not support bold; simplify DrawText and GetExtent accordingly.
- Fix tab stop counts so all right-column key bindings align correctly.

Auth:
- On InvalidSession refresh failure, probe the QUOTA endpoint before
  wiping the session; HTTP 200 means the token is still live and the
  client proceeds logged in rather than forcing an unnecessary re-login.
- Companion backend change: e-dream-ai/backend#407

Misc: remove unused boost::bind include from Player.cpp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…efactor

Upstream added richer result types (SendCodeResult, ValidateCodeResult,
HelloResult) and a retry loop in GetCurrentServerPlaylist(). Conflict was
in HelloDetailed(): kept our AppendAuthHeader() call (which handles both
wos-session cookie and Api-Key header for Linux/headless) in place of the
sealed-session-only block from upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alansley
Copy link
Copy Markdown
Author

X11 adjustments made in 9384f5b now allow the client to work correctly in Wayland and X11 - tested on X11 under xfce:

infinidream-client-X11-on-xfce-2026-03-28

@zquestz - thanks for the diff - that was helpful & much appreciated! =D

@scottdraves I'm seeing an issue where sometimes changing from full-screen to windowed then showing the F1 help or F2 status text will sometimes draw the text-backing semi-transparent rectangle as an ellipse for a little while before something "catches up" and it displays as a rectangle again (happens most frequently near startup), but I don't think it's a deal-breaker and can be handled in another PR when I have the energy & patience for it!

@scottdraves
Copy link
Copy Markdown
Contributor

awesome.

yes i saw that ellipse on mac when i was optimizing the Metal drawing and shaders & claude fixed it:
29a5da8

CRendererVulkan::Reset() was a complete no-op, so calling it with
eTexture never cleared m_aspSelectedTextures[]. Apply() then re-bound
the stale video/startup texture, causing DrawSoftQuad to sample it
instead of the white descriptor set — manifesting as a ghost ellipse
behind the F1 help and F2 status overlays.

Fix: delegate to CRenderer::Reset(_flags) so the base class clears
m_aspSelectedTextures, then explicitly reset m_currentDescSet to
m_whiteDescSet when eTexture is requested.

Inspired by upstream commit 29a5da8 (e-dream-ai/client), which fixed
the same class of stale-texture bug on Metal by clearing stale bindings
between draw calls that share a single render encoder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alansley
Copy link
Copy Markdown
Author

yes i saw that ellipse on mac when i was optimizing the Metal drawing and shaders & claude fixed it: 29a5da8

Ah, perfect! I've now made the same adjustment to the Linux client in commit 968c892

@wolfwood
Copy link
Copy Markdown

wolfwood commented Apr 2, 2026

@alansley sure, with the input I was just curious if you had the same experience or if it was a result of me using kanata for input remapping.

are you manually setting a value in .electricsheep/content/json/playlist/playlist_0.json? or is the API key sufficient for your use?

@zquestz
Copy link
Copy Markdown

zquestz commented Apr 2, 2026

I was unable to use an API key as well. I had to use an existing session.

@wolfwood
Copy link
Copy Markdown

wolfwood commented Apr 2, 2026

@zquestz that did it, thanks for the pointer!

@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 3, 2026

You can launch with an API key via:
INFINIDREAM_API_KEY=<your-key> ./infinidream

@wolfwood
Copy link
Copy Markdown

wolfwood commented Apr 3, 2026

I'm using the environment variable, but I got a black screen and the session issue shown in the log I shared.

the client plays dreams successfully when, in addition to the env var API key, I supply an existing session ID as a value in .electricsheep/settings.json for key settings.content.sealed_session.

@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 3, 2026

@wolfwood - fair enough, thanks for the details, I'll look into it.

@zquestz
Copy link
Copy Markdown

zquestz commented Apr 3, 2026

I'm using the environment variable, but I got a black screen and the session issue shown in the log I shared.

the client plays dreams successfully when, in addition to the env var API key, I supply an existing session ID as a value in .electricsheep/settings.json for key settings.content.sealed_session.

This is the behavior I see as well.

@scottdraves
Copy link
Copy Markdown
Contributor

cool! i got a box set up &\ was able to build and run pretty easily on ubuntu. however i didn't have easy access to a sealed session from another machine, so i didn't get any videos.

@wolfwood
Copy link
Copy Markdown

wolfwood commented Apr 4, 2026

@scottdraves you can grab a session cookie by visiting https://api-alpha.infinidream.ai/api/v1/client/hello in a browser that is logged in to infinidream and then using the Web Developer Tools -> storage tab -> cookies. it's the value for the key wos-session.

@scottdraves
Copy link
Copy Markdown
Contributor

yea it works if i copy it from a browser, then it downloads and displays at a really low frame rate.
maybe missing the cubic interpolation?

amillionbouncyballs and others added 2 commits April 5, 2026 07:07
…g path

- Add LoginWithMagicLinkCode(): on first run prompts for email (saved to
  settings.generator.nickname), sends a one-time code, reads it from stdin,
  and exchanges it for a sealed session before the window opens. Subsequent
  runs auto-refresh the session; re-prompts interactively if it expires.
- Add --cached flag: skips auth entirely and plays locally cached videos in
  offline mode, reporting count and size ("cycling through N (X.X GB) of
  cached videos"). Exits with a clear message if the cache is empty.
- Pre-check auth state before the display window opens: if no sealed session
  and no API key, attempt magic link login or abort with actionable guidance
  rather than opening a window that cannot download any content.
- Store api_key and user email in settings.json (settings.content.api_key /
  settings.generator.nickname) rather than a separate credentials file.
- Fix Linux settings path: remove hardcoded ~/.electricsheep/ in InitStorage()
  so settings, logs, and content all live under ~/.config/infinidream/ as
  client_linux.h always intended.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 4, 2026

Added interactive login flow to obtain sealed session token if missing - 283742a

image image

I'm not sure why it's doing 20 FPS, but I'd prefer to investigate that as a separate issue.

@scottdraves
Copy link
Copy Markdown
Contributor

great, will try later. The "decoding video at" 20.0 fps is correct, that's just the default.
But "cubic display at 20 fps" doesn't seam right, that should be at whatever the display's refresh rate is, like 30 or 60.

@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 4, 2026

I'm using VRR via GSync atm, could well be confusing the issue:
image

Anyways, let me know how you get on.

@scottdraves
Copy link
Copy Markdown
Contributor

The sign-in flow worked, awesome :)

if we could get sync with infinidream's playback for extra smoothness that would be awesome but yea, it might require a little push.

but even without that, i am seeing really poor display, i think it started out at 10-20fps but slowed down to ~5 after a bit.

@scottdraves
Copy link
Copy Markdown
Contributor

Log has:

[2026-04-05 22:16:32.849871 - error]: FFmpeg error: Failed to initialise VAAPI connection: -1 (unknown libva error).
[2026-04-05 22:16:32.849976 - info]: VAAPI unavailable — using software decoding

Previously when it initialized Vulkan it said:

[2026-04-05 22:16:29.259853 - info]: Attempting to open Vulkan display...
[2026-04-05 22:16:29.260183 - info]: CDisplayVulkan: Wayland global: wl_compositor (v6)
[2026-04-05 22:16:29.260213 - info]: CDisplayVulkan: Wayland global: wl_shm (v2)
[2026-04-05 22:16:29.260227 - info]: CDisplayVulkan: Wayland global: zxdg_output_manager_v1 (v3)
[2026-04-05 22:16:29.260239 - info]: CDisplayVulkan: Wayland global: wl_data_device_manager (v3)
[2026-04-05 22:16:29.260251 - info]: CDisplayVulkan: Wayland global: xdg_toplevel_drag_manager_v1 (v1)
[2026-04-05 22:16:29.260263 - info]: CDisplayVulkan: Wayland global: zwp_primary_selection_device_manager_v1 (v1)
[2026-04-05 22:16:29.260274 - info]: CDisplayVulkan: Wayland global: wl_subcompositor (v1)
[2026-04-05 22:16:29.260285 - info]: CDisplayVulkan: Wayland global: xdg_wm_base (v7)
[2026-04-05 22:16:29.260297 - info]: CDisplayVulkan: Wayland global: gtk_shell1 (v6)
[2026-04-05 22:16:29.260309 - info]: CDisplayVulkan: Wayland global: wp_viewporter (v1)
[2026-04-05 22:16:29.260320 - info]: CDisplayVulkan: Wayland global: wp_fractional_scale_manager_v1 (v1)
[2026-04-05 22:16:29.260332 - info]: CDisplayVulkan: Wayland global: zwp_pointer_gestures_v1 (v3)
[2026-04-05 22:16:29.260343 - info]: CDisplayVulkan: Wayland global: zwp_tablet_manager_v2 (v2)
[2026-04-05 22:16:29.260354 - info]: CDisplayVulkan: Wayland global: wp_pointer_warp_v1 (v1)
[2026-04-05 22:16:29.260365 - info]: CDisplayVulkan: Wayland global: wl_seat (v10)
[2026-04-05 22:16:29.260377 - info]: CDisplayVulkan: Wayland global: zwp_relative_pointer_manager_v1 (v1)
[2026-04-05 22:16:29.260389 - info]: CDisplayVulkan: Wayland global: zwp_pointer_constraints_v1 (v1)
[2026-04-05 22:16:29.260400 - info]: CDisplayVulkan: Wayland global: zxdg_exporter_v2 (v1)
[2026-04-05 22:16:29.260411 - info]: CDisplayVulkan: Wayland global: zxdg_importer_v2 (v1)
[2026-04-05 22:16:29.260423 - info]: CDisplayVulkan: Wayland global: zxdg_exporter_v1 (v1)
[2026-04-05 22:16:29.260434 - info]: CDisplayVulkan: Wayland global: zxdg_importer_v1 (v1)
[2026-04-05 22:16:29.260445 - info]: CDisplayVulkan: Wayland global: zwp_linux_dmabuf_v1 (v5)
[2026-04-05 22:16:29.260456 - info]: CDisplayVulkan: Wayland global: wp_single_pixel_buffer_manager_v1 (v1)
[2026-04-05 22:16:29.260467 - info]: CDisplayVulkan: Wayland global: zwp_keyboard_shortcuts_inhibit_manager_v1 (v1)
[2026-04-05 22:16:29.260479 - info]: CDisplayVulkan: Wayland global: zwp_text_input_manager_v3 (v1)
[2026-04-05 22:16:29.260490 - info]: CDisplayVulkan: Wayland global: wp_presentation (v2)
[2026-04-05 22:16:29.260501 - info]: CDisplayVulkan: Wayland global: xdg_activation_v1 (v1)
[2026-04-05 22:16:29.260512 - info]: CDisplayVulkan: Wayland global: zwp_idle_inhibit_manager_v1 (v1)
[2026-04-05 22:16:29.260523 - info]: CDisplayVulkan: Wayland global: wp_linux_drm_syncobj_manager_v1 (v1)
[2026-04-05 22:16:29.260535 - info]: CDisplayVulkan: Wayland global: xdg_wm_dialog_v1 (v1)
[2026-04-05 22:16:29.260546 - info]: CDisplayVulkan: Wayland global: wp_color_manager_v1 (v1)
[2026-04-05 22:16:29.260557 - info]: CDisplayVulkan: Wayland global: xdg_system_bell_v1 (v1)
[2026-04-05 22:16:29.260568 - info]: CDisplayVulkan: Wayland global: xdg_toplevel_tag_manager_v1 (v1)
[2026-04-05 22:16:29.260579 - info]: CDisplayVulkan: Wayland global: wp_drm_lease_device_v1 (v1)
[2026-04-05 22:16:29.260591 - info]: CDisplayVulkan: Wayland global: wp_commit_timing_manager_v1 (v1)
[2026-04-05 22:16:29.260602 - info]: CDisplayVulkan: Wayland global: wp_fifo_manager_v1 (v1)
[2026-04-05 22:16:29.260613 - info]: CDisplayVulkan: Wayland global: wp_cursor_shape_manager_v1 (v2)
[2026-04-05 22:16:29.260624 - info]: CDisplayVulkan: Wayland global: wp_color_representation_manager_v1 (v1)
[2026-04-05 22:16:29.260636 - info]: CDisplayVulkan: Wayland global: wl_fixes (v1)
[2026-04-05 22:16:29.260656 - info]: CDisplayVulkan: Wayland global: wl_output (v4)
[2026-04-05 22:16:29.260800 - warning]: CDisplayVulkan: xdg-decoration not supported by compositor — windowed mode will lack borders
[2026-04-05 22:16:29.267956 - info]: CDisplayVulkan: onXdgToplevelConfigure 3840x2160 fullscreen=1 (want=1)
[2026-04-05 22:16:29.268023 - info]: CDisplayVulkan: Wayland initialized 3840x2160 (fullscreen=1)
[2026-04-05 22:16:29.310526 - info]: CRendererVulkan: selected GPU: Intel(R) Graphics (ADL-N)
[2026-04-05 22:16:29.348144 - info]: CRendererVulkan: ImGui initialized
[2026-04-05 22:16:29.348575 - info]: CRendererVulkan: initialized (3840x2160)

@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 6, 2026

i am seeing really poor display, i think it started out at 10-20fps but slowed down to ~5 after a bit.

Sounds like it might be thermal throttling due to the CPU-based video decoding.

You can try a Hail-Mary fix first if you want - it's not going to break anything any worse.

For example, try forcing the modern iHD video driver in case you're on the legacy i965 driver:

LIBVA_DRIVER_NAME=iHD ./infinidream

Or unset the LIBVA_DRIVER_NAME in case whatever's currently set is wrong:

env -u LIBVA_DRIVER_NAME ./infinidream

If that doesn't get the hardware video decoding back...

  1. ) Make sure you have the most recent intel graphics card driver:
sudo apt update
sudo apt install intel-media-driver vainfo

2.) Figure out what the current setup is (outputs details to vaapi-debug.txt):

(
  echo "=== Session / VAAPI env ==="
  echo "XDG_SESSION_TYPE=$XDG_SESSION_TYPE"
  echo "LIBVA_DRIVER_NAME=$LIBVA_DRIVER_NAME"
  echo "LIBVA_DRIVERS_PATH=$LIBVA_DRIVERS_PATH"
  echo

  echo "=== GPU ==="
  lspci -k | grep -EA3 'VGA|3D|Display' || true
  echo

  echo "=== Installed video / VAAPI packages ==="
  dpkg -l | grep -E 'libva|vainfo|intel-media|i965|mesa' || true
  echo

  echo "=== vainfo (default) ==="
  vainfo || true
  echo

  echo "=== vainfo (--display drm) ==="
  vainfo --display drm || true
  echo

  echo "=== vainfo (forced iHD) ==="
  LIBVA_DRIVER_NAME=iHD vainfo || true
  echo

  echo "=== vainfo (forced i965) ==="
  LIBVA_DRIVER_NAME=i965 vainfo || true
  echo
) 2>&1 | tee vaapi-debug.txt

Let me know what the above says and I can look into it - or give that info to Claude or ChatGPT - they can help figure stuff out, too.

@scottdraves
Copy link
Copy Markdown
Contributor

just had to do

sudo apt install intel-media-va-driver vainfo

and the error goes away,
but the problem wasn't fixed.

The Linux client was doing too much synchronous work on the main/render loop for what should be a mostly idle video screensaver workload. In particular, the shared client frame path was repeatedly probing internet reachability, rebuilding cache statistics, polling download/network status, and updating large HUD/stat blocks every frame. On Linux that showed up as excessive CPU usage even when hardware video decode was active, and it also created extra event-pump churn because the Linux wrapper called Display()->Update() a second time each frame.

This change introduces a cached RuntimeDiagnostics layer inside CElectricSheep so the expensive shared-loop diagnostics are sampled on coarse timers instead of per frame. Fast-changing values such as CPU, GPU, and transfer status are refreshed at a short interval, while slower values such as internet reachability, disk/update status, and cache size are refreshed at longer intervals. The dream stats and credits HUD updates are also gated on HUD visibility so hidden overlays no longer pay the full per-frame formatting and data collection cost. An optional INFINIDREAM_PERF_LOG path remains available for coarse runtime measurements.

On Linux specifically, the extra Display()->Update() call in CElectricSheep_Linux::Update() is removed so window events are pumped once per frame through the shared render path instead of twice. That reduces unnecessary X11/Wayland work without changing presentation semantics.

This is intentionally safe for macOS because it does not change the Metal renderer, macOS display update path, swap/present behavior, or decode pipeline. The shared-code changes only reduce how often diagnostic state is recomputed and how often hidden HUDs are updated. Visible HUD behavior remains intact, user-triggered network checks still force a fresh connectivity probe, and the Linux-only event-pump change is isolated to client_linux.h.
@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 6, 2026

The main thread's very busy and doing a lot of work per-frame that doesn't need to be per-frame - fixed in: 994b1a9

Try rebuilding with the above commit then run:
INFINIDREAM_PERF_LOG=1 ./infinidream

My before stats:

[2026-Apr-07 05:46:42.305208 - info]: Perf: avg=29.69ms 33.68fps hud=0.09ms cpu=51% total=7% gpu=0.01ms
[2026-Apr-07 05:46:47.317562 - info]: Perf: avg=25.80ms 38.75fps hud=0.08ms cpu=64% total=10% gpu=0.01ms
[2026-Apr-07 05:46:52.327578 - info]: Perf: avg=25.59ms 39.09fps hud=0.07ms cpu=47% total=10% gpu=0.01ms

My after stats:

[2026-Apr-07 05:51:30.745739 - info]: Perf: avg=4.41ms 226.51fps hud=0.09ms cpu=52% total=7% gpu=0.01ms
[2026-Apr-07 05:51:35.754601 - info]: Perf: avg=4.61ms 216.69fps hud=0.09ms cpu=56% total=8% gpu=0.01ms
[2026-Apr-07 05:51:40.765903 - info]: Perf: avg=4.34ms 230.36fps hud=0.09ms cpu=53% total=7% gpu=0.01ms

Wayland windowed mode previously relied entirely on xdg-decoration server-side decorations. On compositors that do not provide that protocol, the Linux client came up undecorated, which left the window difficult to move and resize compared with the existing X11 path.

Add an optional libdecor-0 fallback in the Vulkan display backend and Linux build. When xdg-decoration is unavailable, the Wayland path now creates a libdecor frame, routes title/fullscreen updates through it, dispatches libdecor events during the frame loop, and tears it down safely without affecting the X11 path.

Also add Wayland cursor-theme handling for the content surface so the app restores a normal arrow when re-entered from another application, reports a distro-correct warning when libdecor decorations cannot be provided, and exposes a content-side resize hotzone near the window edges and corners to make resizing practical even when the decoration backend's own hit targets are narrow.

This change is Linux-only. macOS is unaffected because the code paths are guarded behind the Wayland/Linux build configuration and do not alter the shared Metal or Cocoa client behavior.
@alansley
Copy link
Copy Markdown
Author

alansley commented Apr 6, 2026

To build with libdecor install the package of the same name in Arch, or for Ubuntu you'll need to install something like:
sudo apt install libdecor-0-0 libdecor-0-plugin-1-gtk libdecor-0-dev

Users won't need the dev package, but it is required to build with libdecor support. If missing there's just a warning that Wayland windows will not have decorations - it doesn't prevent building or anything.

There is an issue where moving the mouse from inside the display surface to the edge and then continuing outside the window can display the mouse pointer as: Resize-related -> Normal -> Resize-related-again. This is more of a libdecor issue than a this-code issue:

On the problematic top/left edges, the pointer is effectively moving through three zones:

  1. Infinidream content hotzone.
     We control this.
     This is the 12px in-app resize band inside the content surface, where we set the resize cursor and call libdecor_frame_resize(...).
  2. libdecor frame interior.
     libdecor controls this.
     This is the decoration/titlebar/border area outside our content but not yet in the outer resize border. Here libdecor chooses the cursor, and that’s where you were seeing it fall back
     to a normal arrow.
  3. libdecor outer resize border.
     libdecor controls this too.
     This is the actual outer frame edge/corner resize region, where libdecor switches back to resize cursors and interactive edge resizing.

  So the awkward resize -> arrow -> resize transition is:

  - our content hotzone
  - then libdecor’s non-resize decoration area
  - then libdecor’s own outer resize zone

Not ideal, but not a deal-breaker. Looks and works fine otherwise:
image

@scottdraves scottdraves mentioned this pull request Apr 12, 2026
amillionbouncyballs and others added 2 commits April 21, 2026 14:29
…text

After merging upstream/master, resolve several issues introduced during
conflict resolution:

- Guard POSIX-only code in EDreamClient.cpp (#include <unistd.h>,
  LoginWithMagicLinkCode body) so Windows builds are not broken
- Guard the Linux headless auth block in CElectricSheep::Startup() with
  #ifdef LINUX_GNU so Mac/Windows first-run startup is not short-circuited
  before the GUI auth flow can run
- Restore null guards on g_Player().Renderer() and g_Player().Display()
  in StartupScreen constructor and CElectricSheep::Run()
- Remove DrawSoftQuad override keyword from RendererVulkan (upstream
  removed the base class virtual in commit 1b4735d)
- Fix Frame.h constructor assigning _width/_height to AVFrame fields
- Fix duplicate ToggleFullscreen in DisplayOutput.h (bool vs void return)
- Restore startup screen text rendering: draw "Connecting to
  infinidream.ai..." centered at top of screen via GetExtent() + DrawText

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@amillionbouncyballs
Copy link
Copy Markdown

amillionbouncyballs commented Apr 21, 2026

@scottdraves - the Linux client has now been consolidated with upstream changes.

When this is in I'll raise the AppImage work as a separate PR.

@scottdraves
Copy link
Copy Markdown
Contributor

awesome but i tried it on mac and it had problems, crashed right away. claude fixed that but then it wasn't responding to remote control commands. maybe i should try before the windows merge?

diff --git a/client_generic/ContentDecoder/ContentDecoder.cpp b/client_generic/ContentDecoder/ContentDecoder.cpp
index f381d22a..ac872398 100644
--- a/client_generic/ContentDecoder/ContentDecoder.cpp
+++ b/client_generic/ContentDecoder/ContentDecoder.cpp
@@ -355,6 +355,23 @@ bool CContentDecoder::Open()
             g_Log->Info("VAAPI unavailable — using software decoding");
         }
     }
+#elif defined(MAC)
+    // Attach VideoToolbox hw_device_ctx so FFmpeg's default get_format picks
+    // AV_PIX_FMT_VIDEOTOOLBOX for supported codecs, delivering frames as
+    // CVPixelBufferRef in AVFrame::data[3] — which TextureFlatMetal expects.
+    {
+        AVHWDeviceType hw_type = av_hwdevice_find_type_by_name("videotoolbox");
+        if (hw_type != AV_HWDEVICE_TYPE_NONE)
+        {
+            ovi->m_pVideoCodecContext->hw_device_ctx =
+                av_hwdevice_ctx_alloc(hw_type);
+            av_hwdevice_ctx_init(ovi->m_pVideoCodecContext->hw_device_ctx);
+        }
+        else
+        {
+            g_Log->Error("VideoToolbox hardware acceleration unsupported.");
+        }
+    }
 #endif
     // Initialize the codec context
     ovi->m_pFormatContext->flags |= AVFMT_FLAG_IGNIDX;   //    Ignore index.

Our earlier work replaced the upstream `if (USE_HW_ACCELERATION)` block
with a `#ifdef LINUX_GNU` VAAPI block, which inadvertently deleted the
Mac VideoToolbox initialisation.  Without it, FFmpeg outputs software
YUV frames.  TextureFlatMetal::GetPixelBuffer() casts data[3] directly
to CVPixelBufferRef; for software frames data[3] is null, causing an
immediate crash the moment the first video starts playing.  Once the
app crashes the main loop stops running and ProcessCommandQueue() is
never called, which is why remote-control commands also appeared broken.

Fix:
- Add a vt_get_format() callback (MAC-only, mirrors the existing
  vaapi_get_format for Linux) so the codec negotiates
  AV_PIX_FMT_VIDEOTOOLBOX rather than falling back to software YUV.
- Restore VideoToolbox hw-device creation in ContentDecoder::Open()
  using av_hwdevice_ctx_create() (the correct one-step API, replacing
  the upstream's fragile alloc+init pattern) guarded with
  #elif defined(MAC).

The Linux VAAPI path is untouched; Windows gets neither block and
continues with software decoding as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@amillionbouncyballs
Copy link
Copy Markdown

awesome but i tried it on mac and it had problems, crashed right away. claude fixed that but then it wasn't responding to > remote control commands. maybe i should try before the windows merge?

Oops - fixed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants