Compare commits

...

18 Commits

Author SHA1 Message Date
Quantum
4bb2c58fb6 [client] wayland: delete useless enum EGLSwapWithDamageState
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled
2026-06-09 19:31:45 +10:00
Quantum
e203bca480 [client] wayland: set content type to game
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled
2026-06-07 07:04:12 +10:00
Quantum
8d7b45e240 [client] wayland: generate content-type-v1 client protocol 2026-06-07 07:04:12 +10:00
Quantum
66d8a9691e [idd] helper: update notification icon to reflect GPU availability 2026-06-07 07:04:00 +10:00
Quantum
89ddab9d57 [idd] helper: use Resources.h for icon IDs 2026-06-07 07:04:00 +10:00
Quantum
ffffff0740 [resources] icon: add Looking Glass icon with GPU and no GPU superimposed
These will be used as indicators for the IDD helper notification icon to
indicate whether GPU acceleration is available.
2026-06-07 07:04:00 +10:00
Quantum
c8edf1eaf3 [client] wayland: use round half away from zero behaviour
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled
In fractional-scale-v1, the scaling is defined as follows:

> For toplevel surfaces, the size is rounded halfway away from zero.

Previously, it is undefined. This commit makes waylandScaleMulInt follow
the round half away from zero behaviour.
2026-06-06 02:32:23 +10:00
Quantum
938a2a6c22 [client] wayland: get scale from wp-fractional-scale-v1 if possible 2026-06-06 02:32:23 +10:00
Quantum
e406b0fee8 [client] wayland: generate fractional-scale-v1 client protocol 2026-06-06 02:32:23 +10:00
Quantum
79784fa44d [repos] wayland-protocols: bump to latest version 2026-06-06 02:32:23 +10:00
Quantum
a164c02f9a [client] core: resize guest to actual surface size
This ensures that the guest renders with the actual screen size when the
client surface is scaled when using automatic scaling.
2026-06-06 02:31:44 +10:00
Quantum
3aa6492760 [idd] driver: implement reading in pipe server
Following the example of CPipeClient in the helper, this switches to using
overlapped I/O for easy interruption and cancellation that doesn't quite
work with CancelSynchronousIO.
2026-06-06 02:30:43 +10:00
Quantum
0664e510a2 [idd] helper: send message over pipe when settings changed 2026-06-06 02:30:43 +10:00
Quantum
e4e211f07a [doc] build: delete wayland-protocols mentions
We now include it as a submodule, so it's pointless to tell people to
install it separately as a dependency.
2026-06-06 02:29:52 +10:00
Quantum
bff890f635 [idd] helper: use per-monitor DPI awareness v2
This avoids weird issues when scaling menus and non-client areas
dynamically.
2026-06-06 02:29:33 +10:00
Quantum
2317801411 [idd] helper: update icon tip to reflect GPU state 2026-06-06 02:29:14 +10:00
Quantum
140de3199b [idd] helper: defer no GPU notification when needed
No GPU notifications are not possible before the notification icon is
created. Since the helper could start before explorer.exe (and often does
on reboot), the notification would fail.

This commit adds logic to refer the notification and we send it immediately
when successfully creating the notification icon.
2026-06-06 02:29:14 +10:00
Quantum
c8f4898815 [idd] helper: allow changing preferred mode 2026-06-06 02:28:26 +10:00
26 changed files with 360 additions and 81 deletions

View File

@@ -60,6 +60,12 @@ wayland_generate(
wayland_generate(
"${WAYLAND_PROTOCOLS_BASE}/staging/xdg-activation/xdg-activation-v1.xml"
"${CMAKE_BINARY_DIR}/wayland/wayland-xdg-activation-v1-client-protocol")
wayland_generate(
"${WAYLAND_PROTOCOLS_BASE}/staging/fractional-scale/fractional-scale-v1.xml"
"${CMAKE_BINARY_DIR}/wayland/wayland-fractional-scale-v1-client-protocol")
wayland_generate(
"${WAYLAND_PROTOCOLS_BASE}/staging/content-type/content-type-v1.xml"
"${CMAKE_BINARY_DIR}/wayland/wayland-content-type-v1-client-protocol")
target_link_libraries(wayland_protocol
PkgConfig::WAYLAND

View File

@@ -46,6 +46,9 @@ static void registryGlobalHandler(void * data, struct wl_registry * registry,
else if (!strcmp(interface, wp_viewporter_interface.name))
wlWm.viewporter = wl_registry_bind(wlWm.registry, name,
&wp_viewporter_interface, 1);
else if (!strcmp(interface, wp_fractional_scale_manager_v1_interface.name))
wlWm.fractionalScaleManager = wl_registry_bind(wlWm.registry, name,
&wp_fractional_scale_manager_v1_interface, 1);
else if (!strcmp(interface, zwp_relative_pointer_manager_v1_interface.name))
wlWm.relativePointerManager = wl_registry_bind(wlWm.registry, name,
&zwp_relative_pointer_manager_v1_interface, 1);
@@ -68,6 +71,9 @@ static void registryGlobalHandler(void * data, struct wl_registry * registry,
else if (!strcmp(interface, xdg_activation_v1_interface.name))
wlWm.xdgActivation = wl_registry_bind(wlWm.registry, name,
&xdg_activation_v1_interface, 1);
else if (!strcmp(interface, wp_content_type_manager_v1_interface.name))
wlWm.contentTypeManager = wl_registry_bind(wlWm.registry, name,
&wp_content_type_manager_v1_interface, 1);
else if (wlWm.desktop->registryGlobalHandler(
data, registry, name, interface, version))
return;

View File

@@ -71,7 +71,7 @@ static inline int waylandScaleCeil(struct WaylandScale scale)
static inline int waylandScaleMulInt(struct WaylandScale scale, int value)
{
return (int)(((int64_t)value * scale.num) / scale.den);
return (int)(((int64_t)value * scale.num + scale.den / 2) / scale.den);
}
static inline double waylandScaleToDouble(struct WaylandScale scale)

View File

@@ -47,6 +47,8 @@
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
#include "wayland-xdg-output-unstable-v1-client-protocol.h"
#include "wayland-xdg-activation-v1-client-protocol.h"
#include "wayland-fractional-scale-v1-client-protocol.h"
#include "wayland-content-type-v1-client-protocol.h"
#include "scale.h"
@@ -83,13 +85,6 @@ struct SurfaceOutput
struct wl_list link;
};
enum EGLSwapWithDamageState {
SWAP_WITH_DAMAGE_UNKNOWN,
SWAP_WITH_DAMAGE_UNSUPPORTED,
SWAP_WITH_DAMAGE_KHR,
SWAP_WITH_DAMAGE_EXT,
};
struct xkb_context;
struct xkb_keymap;
struct xkb_state;
@@ -175,11 +170,16 @@ struct WaylandDSState
struct wp_viewporter * viewporter;
struct wp_viewport * viewport;
struct wp_fractional_scale_manager_v1 * fractionalScaleManager;
struct wp_fractional_scale_v1 * fractionalScaleInterface;
struct zxdg_output_manager_v1 * xdgOutputManager;
struct wl_list outputs; // WaylandOutput::link
struct wl_list surfaceOutputs; // SurfaceOutput::link
bool useFractionalScale;
struct wp_content_type_manager_v1 * contentTypeManager;
struct wp_content_type_v1 * contentType;
LGEvent * frameEvent;
struct wl_list poll; // WaylandPoll::link

View File

@@ -31,8 +31,21 @@
// Surface-handling listeners.
static void setScale(struct WaylandScale newScale)
{
wlWm.scale = newScale;
wlWm.fractionalScale = waylandScaleIsFractional(newScale);
wlWm.needsResize = true;
waylandCursorScaleChange();
app_invalidateWindow(true);
waylandStopWaitFrame();
}
void waylandWindowUpdateScale(void)
{
if (wlWm.fractionalScaleInterface)
return;
struct WaylandScale maxScale = waylandScaleFromInt(0);
struct SurfaceOutput * node;
@@ -44,14 +57,7 @@ void waylandWindowUpdateScale(void)
}
if (waylandScaleValid(maxScale))
{
wlWm.scale = maxScale;
wlWm.fractionalScale = waylandScaleIsFractional(maxScale);
wlWm.needsResize = true;
waylandCursorScaleChange();
app_invalidateWindow(true);
waylandStopWaitFrame();
}
setScale(maxScale);
}
static void wlSurfaceEnterHandler(void * data, struct wl_surface * surface, struct wl_output * output)
@@ -85,6 +91,16 @@ static const struct wl_surface_listener wlSurfaceListener = {
.leave = wlSurfaceLeaveHandler,
};
static void fractionalScalePreferredScale(void * data,
struct wp_fractional_scale_v1 * fractionalScale, uint32_t scale)
{
setScale(waylandScaleFromRatio(scale, 120));
}
static const struct wp_fractional_scale_v1_listener fractionalScaleListener = {
.preferred_scale = fractionalScalePreferredScale,
};
bool waylandWindowInit(const char * title, const char * appId, bool fullscreen, bool maximize, bool borderless, bool resizable)
{
wlWm.scale = waylandScaleFromInt(1);
@@ -110,7 +126,22 @@ bool waylandWindowInit(const char * title, const char * appId, bool fullscreen,
return false;
}
wl_surface_add_listener(wlWm.surface, &wlSurfaceListener, NULL);
if (wlWm.fractionalScaleManager)
{
wlWm.fractionalScaleInterface = wp_fractional_scale_manager_v1_get_fractional_scale(
wlWm.fractionalScaleManager, wlWm.surface);
wp_fractional_scale_v1_add_listener(wlWm.fractionalScaleInterface,
&fractionalScaleListener, NULL);
}
else
wl_surface_add_listener(wlWm.surface, &wlSurfaceListener, NULL);
if (wlWm.contentTypeManager)
{
wlWm.contentType = wp_content_type_manager_v1_get_surface_content_type(
wlWm.contentTypeManager, wlWm.surface);
wp_content_type_v1_set_content_type(wlWm.contentType, WP_CONTENT_TYPE_V1_TYPE_GAME);
}
if (!wlWm.desktop->shellInit(wlWm.display, wlWm.surface,
title, appId, fullscreen, maximize, borderless, resizable))
@@ -122,6 +153,10 @@ bool waylandWindowInit(const char * title, const char * appId, bool fullscreen,
void waylandWindowFree(void)
{
if (wlWm.fractionalScaleInterface)
wp_fractional_scale_v1_destroy(wlWm.fractionalScaleInterface);
if (wlWm.contentType)
wp_content_type_v1_destroy(wlWm.contentType);
wl_surface_destroy(wlWm.surface);
lgFreeEvent(wlWm.frameEvent);
}

View File

@@ -221,8 +221,8 @@ void core_updatePositionInfo(void)
.type = LG_MSG_WINDOWSIZE,
.windowSize =
{
.width = g_state.windowW,
.height = g_state.windowH
.width = round(g_state.windowW * g_state.windowScale),
.height = round(g_state.windowH * g_state.windowScale)
}
};
lgMessage_post(&msg);

View File

@@ -105,7 +105,6 @@ feature is disabled when running :ref:`cmake <client_building>`.
- ``libxkbcommon-dev``
- ``libwayland-bin``
- ``libwayland-dev``
- ``wayland-protocols``
- Disable with ``cmake -DENABLE_PIPEWIRE=no ..``
@@ -146,7 +145,7 @@ You can fetch these dependencies with the following command:
apt-get install binutils-dev cmake fonts-dejavu-core libfontconfig-dev \
gcc g++ pkg-config libegl-dev libgl-dev libgles-dev libspice-protocol-dev \
nettle-dev libx11-dev libxcursor-dev libxi-dev libxinerama-dev \
libxpresent-dev libxss-dev libxkbcommon-dev libwayland-dev wayland-protocols \
libxpresent-dev libxss-dev libxkbcommon-dev libwayland-dev \
libpipewire-0.3-dev libpulse-dev libsamplerate0-dev
You may omit some dependencies if you disable the feature which requires them

View File

@@ -1,4 +1,4 @@
/**
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
@@ -31,7 +31,8 @@ struct LGPipeMsg
{
SETCURSORPOS,
SETDISPLAYMODE,
GPUSTATUS
GPUSTATUS,
RELOADSETTINGS
}
type;
union

View File

@@ -1,4 +1,4 @@
/**
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
@@ -29,7 +29,7 @@ bool CPipeServer::Init()
m_pipe.Attach(CreateNamedPipeA(
LG_PIPE_NAME,
PIPE_ACCESS_DUPLEX,
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
1,
1024,
@@ -43,6 +43,13 @@ bool CPipeServer::Init()
return false;
}
m_signal.Attach(CreateEvent(NULL, TRUE, FALSE, NULL));
if (!m_signal.IsValid())
{
DEBUG_ERROR_HR(GetLastError(), "Failed to create pipe signal event");
return false;
}
m_running = true;
m_thread.Attach(CreateThread(
NULL,
@@ -66,10 +73,11 @@ void CPipeServer::_DeInit()
{
m_running = false;
m_connected = false;
if (m_signal.IsValid())
SetEvent(m_signal.Get());
if (m_thread.IsValid())
{
CancelSynchronousIo(m_thread.Get());
WaitForSingleObject(m_thread.Get(), INFINITE);
m_thread.Close();
}
@@ -79,6 +87,8 @@ void CPipeServer::_DeInit()
FlushFileBuffers(m_pipe.Get());
m_pipe.Close();
}
m_signal.Close();
}
void CPipeServer::DeInit()
@@ -91,24 +101,47 @@ void CPipeServer::DeInit()
void CPipeServer::Thread()
{
DEBUG_TRACE("Pipe thread started");
HandleT<EventTraits> ioEvent(CreateEvent(NULL, TRUE, FALSE, NULL));
if (!ioEvent.IsValid())
{
DEBUG_ERROR_HR(GetLastError(), "Can't create event for overlapped I/O!");
WaitForSingleObject(m_signal.Get(), 5000);
return;
}
while(m_running)
{
m_connected = false;
bool result = ConnectNamedPipe(m_pipe.Get(), NULL);
DWORD err = GetLastError();
if (!result && err != ERROR_PIPE_CONNECTED)
OVERLAPPED overlapped = { 0 };
overlapped.hEvent = ioEvent.Get();
if (!ConnectNamedPipe(m_pipe.Get(), &overlapped))
{
// if graceful shutdown
if ((err == ERROR_OPERATION_ABORTED && !m_running) ||
err == ERROR_NO_DATA)
DWORD dwError = GetLastError();
switch (dwError) {
case ERROR_PIPE_CONNECTED:
break;
// if timeout
if (err == ERROR_SEM_TIMEOUT)
continue;
DEBUG_FATAL_HR(err, "Error connecting to the named pipe");
break;
case ERROR_IO_PENDING:
{
HANDLE hWait[] = { ioEvent.Get(), m_signal.Get() };
switch (WaitForMultipleObjects(2, hWait, FALSE, INFINITE))
{
case WAIT_OBJECT_0:
break;
case WAIT_OBJECT_0 + 1:
DEBUG_INFO("Connect interrupted by signal");
CancelIo(m_pipe.Get());
WaitForSingleObject(ioEvent.Get(), INFINITE);
continue;
}
break;
}
default:
DEBUG_ERROR_HR(dwError, "Error connecting to the named pipe");
goto end;
}
}
DEBUG_TRACE("Client connected");
@@ -121,14 +154,65 @@ void CPipeServer::Thread()
while (m_running && m_connected)
{
//TODO: Read messages from the client
Sleep(1000);
LGPipeMsg msg;
if (!ReadFile(m_pipe.Get(), &msg, sizeof(msg), NULL, &overlapped))
{
DWORD dwError = GetLastError();
if (dwError != ERROR_IO_PENDING)
{
DEBUG_ERROR_HR(dwError, "ReadFile Failed");
break;
}
HANDLE hWait[] = { ioEvent.Get(), m_signal.Get() };
switch (WaitForMultipleObjects(2, hWait, FALSE, INFINITE))
{
case WAIT_OBJECT_0:
break;
case WAIT_OBJECT_0 + 1:
DEBUG_INFO("I/O interrupted by signal");
CancelIo(m_pipe.Get());
WaitForSingleObject(ioEvent.Get(), INFINITE);
continue;
}
}
DWORD bytesRead;
GetOverlappedResult(m_pipe.Get(), &overlapped, &bytesRead, TRUE);
if (bytesRead != sizeof(msg))
{
DEBUG_ERROR("Corrupted data, expected %lld bytes, read %lld bytes", sizeof msg, bytesRead);
break;
}
if (msg.size != sizeof(msg))
{
DEBUG_ERROR("Corrupted data, expected %lld bytes, actual message size: %lld bytes", sizeof msg, msg.size);
break;
}
switch (msg.type)
{
case LGPipeMsg::RELOADSETTINGS:
HandleReloadSettings();
break;
default:
DEBUG_ERROR("Unknown message type %d", msg.type);
break;
}
}
DEBUG_TRACE("Client disconnected");
DisconnectNamedPipe(m_pipe.Get());
if (m_running)
ResetEvent(m_signal.Get());
}
end:
m_running = false;
m_connected = false;
DEBUG_TRACE("Pipe thread shutdown");
@@ -150,6 +234,7 @@ void CPipeServer::WriteMsg(const LGPipeMsg & msg)
{
DEBUG_WARN_HR(err, "Client disconnected, failed to write");
m_connected = false;
SetEvent(m_signal.Get());
return;
}
@@ -160,6 +245,11 @@ void CPipeServer::WriteMsg(const LGPipeMsg & msg)
FlushFileBuffers(m_pipe.Get());
}
void CPipeServer::HandleReloadSettings()
{
DEBUG_INFO("TODO: reload settings");
}
void CPipeServer::SetCursorPos(uint32_t x, uint32_t y)
{
// do not send cursor messages if we are not connected or they will end up queued

View File

@@ -1,4 +1,4 @@
/**
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
@@ -35,8 +35,9 @@ using namespace Microsoft::WRL::Wrappers::HandleTraits;
class CPipeServer
{
private:
HandleT<HANDLENullTraits> m_pipe;
HandleT<HANDLETraits> m_pipe;
HandleT<HANDLENullTraits> m_thread;
HandleT<EventTraits> m_signal;
std::vector<LGPipeMsg> m_queue;
bool m_running = false;
@@ -49,6 +50,8 @@ class CPipeServer
void WriteMsg(const LGPipeMsg & msg);
void HandleReloadSettings();
public:
~CPipeServer() { DeInit(); }

View File

@@ -35,7 +35,6 @@ bool CConfigWindow::registerClass()
{
WNDCLASSEX wx = {};
populateWindowClass(wx);
wx.hIconSm = wx.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
wx.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wx.lpszClassName = L"LookingGlassIddConfig";
@@ -89,7 +88,7 @@ void CConfigWindow::updateFont()
}
for (HWND child : std::initializer_list<HWND>({
*m_version, *m_modeGroup, *m_modeBox, *m_widthLabel, *m_heightLabel, *m_refreshLabel,
*m_version, *m_modeGroup, *m_modeBox, *m_widthLabel, *m_heightLabel, *m_refreshLabel, *m_modePreferred,
*m_modeWidth, *m_modeHeight, *m_modeRefresh, *m_modeUpdate, *m_modeDelete, *m_modeReset,
*m_autosizeGroup, *m_defRefreshLabel, *m_defRefresh, *m_defRefreshHz,
*m_prefGroup, *m_prefNoGPU,
@@ -97,13 +96,20 @@ void CConfigWindow::updateFont()
SendMessage(child, WM_SETFONT, (WPARAM)m_font.Get(), 1);
}
void CConfigWindow::updateModeList()
int CConfigWindow::updateModeList(int wanted)
{
int result = 0;
m_modeBox->addItem(L"<add new>", -1);
auto &modes = *m_modes;
for (size_t i = 0; i < modes.size(); ++i)
m_modeBox->addItem(modes[i].toString(), i);
{
int idx = m_modeBox->addItem(modes[i].toString(), i);
if (wanted == i)
result = idx;
}
return result;
}
LRESULT CConfigWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
@@ -154,6 +160,7 @@ LRESULT CConfigWindow::onCreate()
m_modeWidth.reset(new CEditWidget(WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_LEFT | ES_NUMBER, m_hwnd));
m_modeHeight.reset(new CEditWidget(WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_LEFT | ES_NUMBER, m_hwnd));
m_modeRefresh.reset(new CEditWidget(WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_LEFT | ES_NUMBER, m_hwnd));
m_modePreferred.reset(new CCheckbox(L"prefer", WS_CHILD | WS_VISIBLE, m_hwnd));
m_modeUpdate.reset(new CButton(L"Save", WS_CHILD | WS_VISIBLE | WS_TABSTOP, m_hwnd));
m_modeDelete.reset(new CButton(L"Delete", WS_CHILD | WS_VISIBLE | WS_TABSTOP, m_hwnd));
@@ -206,6 +213,7 @@ LRESULT CConfigWindow::onResize(DWORD width, DWORD height)
pos.pinBottomLeft(*m_modeWidth, 75, 96, 50, 20);
pos.pinBottomLeft(*m_modeHeight, 75, 72, 50, 20);
pos.pinBottomLeft(*m_modeRefresh, 75, 48, 50, 20);
pos.pinBottomLeft(*m_modePreferred, 130, 96, 70, 20);
pos.pinBottomLeft(*m_modeUpdate, 24, 20, 50, 24);
pos.pinBottomLeft(*m_modeDelete, 75, 20, 50, 24);
pos.pinBottomLeft(*m_modeReset, 126, 20, 50, 24);
@@ -238,6 +246,7 @@ void CConfigWindow::onModeListSelectChange()
m_modeWidth->setNumericValue(mode.width);
m_modeHeight->setNumericValue(mode.height);
m_modeRefresh->setNumericValue(mode.refresh);
m_modePreferred->setChecked(mode.preferred);
}
EnableWindow(*m_modeUpdate, TRUE);
EnableWindow(*m_modeDelete, index >= 0);
@@ -249,6 +258,10 @@ LRESULT CConfigWindow::onCommand(WORD id, WORD code, HWND hwnd)
{
onModeListSelectChange();
}
else if (m_modePreferred && hwnd == *m_modePreferred && code == BN_CLICKED && m_modes)
{
m_modePreferred->setChecked(!m_modePreferred->isChecked());
}
else if (m_modeUpdate && hwnd == *m_modeUpdate && code == BN_CLICKED && m_modes)
{
int sel = m_modeBox->getSel();
@@ -258,24 +271,28 @@ LRESULT CConfigWindow::onCommand(WORD id, WORD code, HWND hwnd)
int index = m_modeBox->getData(sel);
auto &mode = index >= 0 ? (*m_modes)[index] : m_modes->emplace_back();
for (auto &mode : *m_modes)
mode.preferred = false;
try
{
mode.width = m_modeWidth->getNumericValue();
mode.height = m_modeHeight->getNumericValue();
mode.refresh = m_modeRefresh->getNumericValue();
mode.preferred = m_modePreferred->isChecked();
}
catch (std::logic_error&)
{
return 0;
}
if (index >= 0)
m_modeBox->delItem(sel);
m_modeBox->setSel(m_modeBox->addItem(mode.toString().c_str(), index));
m_modeBox->clear();
m_modeBox->setSel(updateModeList(index));
LRESULT result = m_settings.setModes(*m_modes);
if (result != ERROR_SUCCESS)
if (result == ERROR_SUCCESS)
sendSettingChange();
else
DEBUG_ERROR_HR((HRESULT) result, "Failed to save modes");
}
else if (m_modeDelete && hwnd == *m_modeDelete && code == BN_CLICKED && m_modes)
@@ -288,20 +305,27 @@ LRESULT CConfigWindow::onCommand(WORD id, WORD code, HWND hwnd)
m_modeBox->clear();
m_modes->erase(m_modes->begin() + index);
LRESULT result = m_settings.setModes(*m_modes);
if (result != ERROR_SUCCESS)
DEBUG_ERROR_HR((HRESULT) result, "Failed to save modes");
updateModeList();
onModeListSelectChange();
LRESULT result = m_settings.setModes(*m_modes);
if (result == ERROR_SUCCESS)
sendSettingChange();
else
DEBUG_ERROR_HR((HRESULT) result, "Failed to save modes");
}
else if (m_modeReset && hwnd == *m_modeReset && code == BN_CLICKED && m_modes)
{
*m_modes = m_settings.getDefaultModes();
m_settings.setModes(*m_modes);
m_modeBox->clear();
updateModeList();
onModeListSelectChange();
LRESULT result = m_settings.setModes(*m_modes);
if (result == ERROR_SUCCESS)
sendSettingChange();
else
DEBUG_ERROR_HR((HRESULT)result, "Failed to save modes");
}
else if (m_defRefresh && hwnd == *m_defRefresh && code == EN_CHANGE && m_defaultRefresh)
{
@@ -316,8 +340,11 @@ LRESULT CConfigWindow::onCommand(WORD id, WORD code, HWND hwnd)
}
m_defaultRefresh = value;
LRESULT result = m_settings.setDefaultRefresh(value);
if (result != ERROR_SUCCESS)
if (result == ERROR_SUCCESS)
sendSettingChange();
else
DEBUG_ERROR_HR((HRESULT)result, "Failed to default refresh");
}
else if (m_prefNoGPU && hwnd == *m_prefNoGPU && code == BN_CLICKED && m_noGPU)

View File

@@ -49,6 +49,7 @@ class CConfigWindow : public CWindow
std::unique_ptr<CEditWidget> m_modeWidth;
std::unique_ptr<CEditWidget> m_modeHeight;
std::unique_ptr<CEditWidget> m_modeRefresh;
std::unique_ptr<CCheckbox> m_modePreferred;
std::unique_ptr<CButton> m_modeUpdate;
std::unique_ptr<CButton> m_modeDelete;
@@ -63,6 +64,8 @@ class CConfigWindow : public CWindow
std::unique_ptr<CCheckbox> m_prefNoGPU;
std::function<void()> m_onDestroy;
std::function<void()> m_onSettingChange;
double m_scale;
Microsoft::WRL::Wrappers::HandleT<FontTraits> m_font;
CRegistrySettings m_settings;
@@ -72,8 +75,9 @@ class CConfigWindow : public CWindow
void getMinimumSize(LONG &width, LONG &height);
void updateFont();
void updateModeList();
int updateModeList(int wanted = -1);
void onModeListSelectChange();
void sendSettingChange() { if (m_onSettingChange) m_onSettingChange(); }
virtual LRESULT handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override;
virtual LRESULT onCreate() override;
@@ -86,4 +90,5 @@ public:
static bool registerClass();
void onDestroy(std::function<void()> func) { m_onDestroy = std::move(func); }
void onSettingChange(std::function<void()> func) { m_onSettingChange = std::move(func); }
};

View File

@@ -20,6 +20,7 @@
#include "CNotifyWindow.h"
#include "CConfigWindow.h"
#include "Resources.h"
#include <CDebug.h>
#include <windowsx.h>
#include <strsafe.h>
@@ -48,8 +49,8 @@ bool CNotifyWindow::registerClass()
return s_atom;
}
CNotifyWindow::CNotifyWindow() : m_iconData({ 0 }), m_menu(CreatePopupMenu()),
closeRequested(false)
CNotifyWindow::CNotifyWindow() : m_iconData({ 0 }), m_iconRegistered(false),
m_menu(CreatePopupMenu()), closeRequested(false)
{
CreateWindowEx(0, MAKEINTATOM(s_atom), NULL,
0, 0, 0, 0, 0, NULL, NULL, hInstance, this);
@@ -82,7 +83,7 @@ LRESULT CNotifyWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
return 0;
case WM_NO_GPU:
handleNoGPUNotification();
handleGPUNotification((bool)wParam);
return 0;
default:
@@ -140,6 +141,8 @@ LRESULT CNotifyWindow::onNotifyIcon(UINT uEvent, WORD wIconId, int x, int y)
m_config->onDestroy([this]() {
PostMessage(m_hwnd, WM_CLEAN_UP_CONFIG, 0, 0);
});
if (m_onSettingChange)
m_config->onSettingChange(m_onSettingChange);
ShowWindow(*m_config, SW_NORMAL);
break;
}
@@ -152,9 +155,9 @@ void CNotifyWindow::registerIcon()
{
m_iconData.cbSize = sizeof m_iconData;
m_iconData.hWnd = m_hwnd;
m_iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
m_iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP;
m_iconData.uCallbackMessage = WM_NOTIFY_ICON;
m_iconData.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
m_iconData.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(ID_MAIN_ICON));
m_iconData.uVersion = NOTIFYICON_VERSION_4;
StringCbCopy(m_iconData.szTip, sizeof m_iconData.szTip, L"Looking Glass (IDD)");
@@ -164,17 +167,45 @@ void CNotifyWindow::registerIcon()
return;
}
m_iconRegistered = true;
if (!Shell_NotifyIcon(NIM_SETVERSION, &m_iconData))
DEBUG_ERROR_HR(GetLastError(), "Shell_NotifyIcon(NIM_SETVERSION)");
if (m_gpuQueue)
{
handleGPUNotification(*m_gpuQueue);
m_gpuQueue.reset();
}
}
void CNotifyWindow::noGPUNotification()
void CNotifyWindow::setGPU(bool hasGPU)
{
PostMessage(m_hwnd, WM_NO_GPU, 0, 0);
if (m_iconRegistered)
PostMessage(m_hwnd, WM_NO_GPU, hasGPU, 0);
else
m_gpuQueue.emplace(hasGPU);
}
void CNotifyWindow::handleNoGPUNotification()
void CNotifyWindow::handleGPUNotification(bool hasGPU)
{
StringCbCopy(m_iconData.szTip, sizeof m_iconData.szTip, hasGPU ?
L"Looking Glass (IDD) with GPU acceleration" :
L"Looking Glass (IDD) with software rendering");
if (hasGPU)
m_iconData.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(ID_GPU_ICON));
else
m_iconData.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(ID_NO_GPU_ICON));
if (!Shell_NotifyIcon(NIM_MODIFY, &m_iconData))
{
DEBUG_ERROR_HR(GetLastError(), "Shell_NotifyIcon(NIM_ADD)");
return;
}
if (hasGPU)
return;
CRegistrySettings settings;
LSTATUS error = settings.open();
if (error != ERROR_SUCCESS)
@@ -187,7 +218,7 @@ void CNotifyWindow::handleNoGPUNotification()
NOTIFYICONDATA nid;
memcpy(&nid, &m_iconData, sizeof nid);
nid.uFlags = NIF_INFO;
nid.uFlags = NIF_INFO | NIF_SHOWTIP;
nid.dwInfoFlags = NIIF_WARNING;
StringCbCopy(nid.szInfoTitle, sizeof nid.szInfoTitle, L"No GPU found!");
StringCbCopy(nid.szInfo, sizeof nid.szInfo,

View File

@@ -20,7 +20,9 @@
#pragma once
#include "CWindow.h"
#include <functional>
#include <memory>
#include <optional>
class CConfigWindow;
@@ -30,13 +32,17 @@ class CNotifyWindow : public CWindow
static ATOM s_atom;
NOTIFYICONDATA m_iconData;
bool m_iconRegistered;
std::optional<bool> m_gpuQueue;
HMENU m_menu;
bool closeRequested;
std::unique_ptr<CConfigWindow> m_config;
std::function<void()> m_onSettingChange;
LRESULT onNotifyIcon(UINT uEvent, WORD wIconId, int x, int y);
void registerIcon();
void handleNoGPUNotification();
void handleGPUNotification(bool hasGPU);
virtual LRESULT handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override;
virtual LRESULT onCreate() override;
@@ -62,8 +68,10 @@ public:
static bool registerClass();
void noGPUNotification();
void setGPU(bool hasGPU);
HWND hwndDialog();
void close();
void onSettingChange(std::function<void()> func) { m_onSettingChange = std::move(func); }
};

View File

@@ -170,6 +170,17 @@ void CPipeClient::WriteMsg(const LGPipeMsg& msg)
FlushFileBuffers(m_pipe.Get());
}
void CPipeClient::ReloadSettings()
{
if (!m_connected)
return;
LGPipeMsg msg;
msg.size = sizeof(msg);
msg.type = LGPipeMsg::RELOADSETTINGS;
WriteMsg(msg);
}
void CPipeClient::Thread()
{
DEBUG_INFO("Pipe thread started");
@@ -308,6 +319,5 @@ void CPipeClient::HandleSetDisplayMode(const LGPipeMsg& msg)
void CPipeClient::HandleGPUStatus(const LGPipeMsg& msg)
{
if (msg.gpuStatus.software)
CNotifyWindow::instance().noGPUNotification();
CNotifyWindow::instance().setGPU(!msg.gpuStatus.software);
}

View File

@@ -59,6 +59,8 @@ public:
bool Init();
void DeInit();
bool IsRunning() { return m_running; }
void ReloadSettings();
};
extern CPipeClient g_pipe;

View File

@@ -22,6 +22,7 @@
#include <windowsx.h>
#include <strsafe.h>
#include <CDebug.h>
#include "Resources.h"
HINSTANCE CWindow::hInstance = (HINSTANCE)GetModuleHandle(NULL);
@@ -30,8 +31,8 @@ void CWindow::populateWindowClass(WNDCLASSEX &wx)
wx.cbSize = sizeof(WNDCLASSEX);
wx.lpfnWndProc = wndProc;
wx.hInstance = hInstance;
wx.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wx.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
wx.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(ID_MAIN_ICON));
wx.hIconSm = wx.hIcon;
wx.hCursor = LoadCursor(NULL, IDC_ARROW);
wx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@@ -91,7 +91,8 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
</PostBuildEvent>
<Manifest />
<Manifest>
<EnableDpiAwareness>PerMonitorHighDPIAware</EnableDpiAwareness>
<EnableDpiAwareness>false</EnableDpiAwareness>
<InputResourceManifests>$(ProjectDir)HighDPI.manifest</InputResourceManifests>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
@@ -118,7 +119,8 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
</PostBuildEvent>
<Manifest />
<Manifest>
<EnableDpiAwareness>PerMonitorHighDPIAware</EnableDpiAwareness>
<EnableDpiAwareness>false</EnableDpiAwareness>
<InputResourceManifests>$(ProjectDir)HighDPI.manifest</InputResourceManifests>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
@@ -144,7 +146,8 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
</PostBuildEvent>
<Manifest />
<Manifest>
<EnableDpiAwareness>PerMonitorHighDPIAware</EnableDpiAwareness>
<EnableDpiAwareness>false</EnableDpiAwareness>
<InputResourceManifests>$(ProjectDir)HighDPI.manifest</InputResourceManifests>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -174,7 +177,8 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
</PostBuildEvent>
<Manifest />
<Manifest>
<EnableDpiAwareness>PerMonitorHighDPIAware</EnableDpiAwareness>
<EnableDpiAwareness>false</EnableDpiAwareness>
<InputResourceManifests>$(ProjectDir)HighDPI.manifest</InputResourceManifests>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
@@ -207,6 +211,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
<ClInclude Include="CRegistrySettings.h" />
<ClInclude Include="CStaticWidget.h" />
<ClInclude Include="CWidget.h" />
<ClInclude Include="Resources.h" />
<ClInclude Include="UIHelpers.h" />
<ClInclude Include="CWindow.h" />
</ItemGroup>
@@ -216,6 +221,9 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
<ItemGroup>
<ResourceCompile Include="resource.rc" />
</ItemGroup>
<ItemGroup>
<Manifest Include="HighDPI.manifest" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
<Target Name="GenerateVersionInfo" BeforeTargets="ClCompile">

View File

@@ -100,6 +100,9 @@
<ClInclude Include="CCheckbox.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Resources.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@@ -109,4 +112,7 @@
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>
<ItemGroup>
<Manifest Include="HighDPI.manifest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* 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, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#pragma once
#define ID_MAIN_ICON 1
#define ID_GPU_ICON 2
#define ID_NO_GPU_ICON 3

View File

@@ -120,6 +120,10 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _
if (!g_pipe.Init())
return EXIT_FAILURE;
window.onSettingChange([]() {
g_pipe.ReloadSettings();
});
HANDLE hWait;
if (!RegisterWaitForSingleObject(&hWait, hParent.Get(), DestroyNotifyWindow, &window, INFINITE, WT_EXECUTEONLYONCE))
DEBUG_ERROR_HR(GetLastError(), "Failed to RegisterWaitForSingleObject");

View File

@@ -22,8 +22,11 @@
#include "winuser.h"
#include "winver.h"
#include "VersionInfo.h"
#include "Resources.h"
IDI_APPLICATION ICON "../../resources/icon.ico"
ID_MAIN_ICON ICON "../../resources/icon.ico"
ID_GPU_ICON ICON "../../resources/icon-gpu.ico"
ID_NO_GPU_ICON ICON "../../resources/icon-nogpu.ico"
#define STRINGIFY2(s) L#s
#define STRINGIFY(s) STRINGIFY2(s)

BIN
resources/icon-gpu.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
resources/icon-nogpu.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB