Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions Tools/windows/Tests/GdiplusStartup.Live.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)][string] $ZigExecutable,
[Parameter(Mandatory)][string] $WinghosttyRoot,
[string] $ScratchRoot = (Join-Path $env:TEMP "graphcode-gdiplus-startup")
)

$ErrorActionPreference = "Stop"

if (-not ("GraphCodeGdiplusStartupNative" -as [type])) {
Add-Type @'
using System;
using System.Runtime.InteropServices;
using System.Text;

public static class GraphCodeGdiplusStartupNative {
public delegate bool EnumWindowsProc(IntPtr window, IntPtr parameter);
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc callback, IntPtr parameter);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassName(IntPtr window, StringBuilder value, int capacity);
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr window);

public static IntPtr Find(uint processId, string className) {
IntPtr result = IntPtr.Zero;
EnumWindows(delegate(IntPtr window, IntPtr parameter) {
uint owner;
GetWindowThreadProcessId(window, out owner);
if (owner != processId || !IsWindowVisible(window)) return true;
var actual = new StringBuilder(128);
GetClassName(window, actual, actual.Capacity);
if (!String.Equals(actual.ToString(), className, StringComparison.Ordinal)) return true;
result = window;
return false;
}, IntPtr.Zero);
return result;
}
}
'@
}

$shellRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..\..\graphcode-windows")
$scratch = [IO.Path]::GetFullPath($ScratchRoot)
$bin = Join-Path $scratch "bin"
$profile = Join-Path $scratch "profile"
$temp = Join-Path $scratch "temp"
$executable = Join-Path $bin "gdiplus-startup-live.exe"
if (Test-Path -LiteralPath $scratch) {
Remove-Item -LiteralPath $scratch -Recurse -Force
}
New-Item -ItemType Directory -Force -Path `
$bin,$profile,(Join-Path $profile "AppData\Local"),(Join-Path $profile "AppData\Roaming"),$temp |
Out-Null

$include = Join-Path $WinghosttyRoot "include"
Push-Location $shellRoot
try {
& $ZigExecutable build-exe src\GdiplusStartupLiveRunner.zig `
-target x86_64-windows-msvc -OReleaseSafe -lc `
-luser32 -lgdi32 -lgdiplus "-I$include" "-femit-bin=$executable"
if ($LASTEXITCODE -ne 0) {
throw "GDI+ startup live runner build failed with exit code $LASTEXITCODE"
}
} finally {
Pop-Location
}

$system32 = Join-Path $env:SystemRoot "System32"
$powerShell = Join-Path $system32 "WindowsPowerShell\v1.0"
$start = [Diagnostics.ProcessStartInfo]::new()
$start.FileName = $executable
$start.WorkingDirectory = $bin
$start.UseShellExecute = $false
$start.Environment.Clear()
$values = [ordered]@{
SystemRoot = $env:SystemRoot
windir = $env:WINDIR
USERPROFILE = $profile
LOCALAPPDATA = Join-Path $profile "AppData\Local"
APPDATA = Join-Path $profile "AppData\Roaming"
TEMP = $temp
TMP = $temp
ProgramData = $env:ProgramData
HOMEDRIVE = [IO.Path]::GetPathRoot($profile).TrimEnd("\")
HOMEPATH = $profile.Substring([IO.Path]::GetPathRoot($profile).Length - 1)
PATH = "$system32;$powerShell;$bin"
}
foreach ($name in $values.Keys) { $start.Environment[$name] = [string]$values[$name] }

$process = [Diagnostics.Process]::new()
$process.StartInfo = $start
$observations = 0
$window = [IntPtr]::Zero
try {
if (-not $process.Start()) { throw "GDI+ startup live runner did not start" }
[void]$process.Handle
$deadline = [DateTime]::UtcNow.AddSeconds(5)
do {
$observations++
$process.Refresh()
if ($process.HasExited) {
throw ("GDI+ startup live runner exited before creating a native window: " +
"exitCode=$($process.ExitCode); observations=$observations")
}
$window = [GraphCodeGdiplusStartupNative]::Find(
[uint32]$process.Id,
"GraphCodeGdiplusStartupTest"
)
if ($window -ne [IntPtr]::Zero) { break }
Start-Sleep -Milliseconds 50
} while ([DateTime]::UtcNow -lt $deadline)

if ($observations -le 0) { throw "GDI+ startup live runner made zero observations" }
if ($window -eq [IntPtr]::Zero) {
throw ("GDI+ startup live runner remained alive but no native window appeared: " +
"observations=$observations; environmentKeys=$(
@($start.Environment.Keys | Sort-Object) -join ',')")
}
Write-Output ("GDIPLUS_STARTUP_LIVE: PASS; executed=1; observations=$observations; " +
"window=$($window.ToInt64()); environmentKeys=$(
@($start.Environment.Keys | Sort-Object) -join ',')")
} finally {
if ($process -and -not $process.HasExited) {
Stop-Process -Id $process.Id -Force
[void]$process.WaitForExit(10000)
}
if ($process) { $process.Dispose() }
Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue
}
13 changes: 12 additions & 1 deletion Tools/windows/Tests/WindowsShell.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ Assert-Contract ($appSource -match 'SetMapMode\(hdc, c\.MM_ANISOTROPIC\)' -and
"custom main-window painting, input, and child layout must share one DPI-scaled coordinate system"
Assert-Contract ($appSource -match
'const uia_gate_hook = envFlag\("GRAPHCODE_UIA_GATE"\);' -and
$appSource -match 'if \(!daemon_supervisor_test_hook and !uia_gate_hook\) GdiplusAA\.init\(\);') `
$appSource -match 'const use_gdiplus = !daemon_supervisor_test_hook and !uia_gate_hook;' -and
$appSource -match 'if \(use_gdiplus\) GdiplusAA\.init\(\);' -and
$appSource -match 'defer if \(use_gdiplus\) GdiplusAA\.deinit\(\);') `
"GDI+ helper-window startup must remain outside daemon-handoff and UIA automation hooks"
Assert-Contract ($appSource -match
'(?s)app\.smoke_tick >= 16 and\s*app\.client\.connectionState\(\) == \.connected and\s*app\.currentProject\(\) != null and app\.model\.selected\(\) != null and\s*!app\.smoke_action_requested') `
Expand Down Expand Up @@ -718,6 +720,15 @@ Invoke-Native "Onboarding executable tests" {
-lc -luser32 -lgdi32 "-I$include"
} finally { Pop-Location }
}
Invoke-Native "GDI+ asynchronous startup live test" {
$winghosttyRoot = [Environment]::GetEnvironmentVariable("GRAPHCODE_WINGHOSTTY_ROOT")
if (-not $winghosttyRoot) {
throw "GRAPHCODE_WINGHOSTTY_ROOT is required for the GDI+ startup live test"
}
& (Join-Path $PSScriptRoot "GdiplusStartup.Live.Tests.ps1") `
-ZigExecutable $zig `
-WinghosttyRoot $winghosttyRoot
}
Invoke-Native "Product Settings executable tests" {
$depotRoot = Split-Path (Split-Path $repoRoot -Parent) -Parent
$winghosttyRoot = [Environment]::GetEnvironmentVariable("GRAPHCODE_WINGHOSTTY_ROOT")
Expand Down
4 changes: 3 additions & 1 deletion graphcode-windows/src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,9 @@ pub const App = struct {
// and UIA live tests depend on deterministic top-level window and
// foreground behavior, so keep that visual-only subsystem disabled
// for both explicit automation hooks.
if (!daemon_supervisor_test_hook and !uia_gate_hook) GdiplusAA.init();
const use_gdiplus = !daemon_supervisor_test_hook and !uia_gate_hook;
if (use_gdiplus) GdiplusAA.init();
defer if (use_gdiplus) GdiplusAA.deinit();
try self.window.create(self, &onWindowMessage, title.ptr);
self.window.key_callback = &onShellKey;
try self.revalidateWorkspaceIdentity();
Expand Down
79 changes: 70 additions & 9 deletions graphcode-windows/src/GdiplusAA.zig
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,68 @@ extern "gdiplus" fn GdipFillPath(graphics: *GpGraphics, brush: *GpBrush, path: *
extern "gdiplus" fn GdipCreateSolidFill(color: u32, brush: **GpBrush) callconv(.winapi) GpStatus;
extern "gdiplus" fn GdipDeleteBrush(brush: *GpBrush) callconv(.winapi) GpStatus;

var startup_mutex: std.Thread.Mutex = .{};
var startup_token: usize = 0;
var available: bool = false;
var start_attempted: bool = false;
var shutdown_requested: bool = false;

/// Attempts to start GDI+ once for the process lifetime. Safe to call
/// repeatedly (a no-op after the first call). All draw functions below
/// check `available` and gracefully return false (do nothing) if this
/// never succeeded, so callers must keep their plain-GDI fallback.
const SystemStartup = struct {
pub fn run() ?usize {
var token: usize = 0;
var input = GdiplusStartupInput{};
if (GdiplusStartup(&token, &input, null) != Ok) return null;
return token;
}
};

/// Starts optional GDI+ initialization away from the UI thread. Until the
/// worker succeeds, every draw call returns false and the caller uses plain GDI.
pub fn init() void {
if (start_attempted) return;
initWith(SystemStartup);
}

pub fn initWith(comptime Startup: type) void {
startup_mutex.lock();
if (start_attempted or shutdown_requested) {
startup_mutex.unlock();
return;
}
start_attempted = true;
var input = GdiplusStartupInput{};
available = GdiplusStartup(&startup_token, &input, null) == Ok;
startup_mutex.unlock();

const Worker = struct {
fn run() void {
const token = Startup.run() orelse return;
startup_mutex.lock();
if (shutdown_requested) {
startup_mutex.unlock();
GdiplusShutdown(token);
return;
}
startup_token = token;
available = true;
startup_mutex.unlock();
}
};
var worker = std.Thread.spawn(.{}, Worker.run, .{}) catch return;
worker.detach();
}

/// Prevents new GDI+ sessions and releases a completed process-global startup.
/// A worker still blocked inside Windows remains detached and cannot delay exit.
pub fn deinit() void {
startup_mutex.lock();
shutdown_requested = true;
if (!available) {
startup_mutex.unlock();
return;
}
available = false;
const token = startup_token;
startup_token = 0;
startup_mutex.unlock();
GdiplusShutdown(token);
}

fn colorrefToArgb(colorref: u32) u32 {
Expand All @@ -94,16 +143,25 @@ const Session = struct {
};

fn beginSession(hdc: c.HDC, colorref: u32, width: f32) ?Session {
if (!available) return null;
startup_mutex.lock();
if (!available) {
startup_mutex.unlock();
return null;
}
var graphics: *GpGraphics = undefined;
if (GdipCreateFromHDC(hdc, &graphics) != Ok) return null;
if (GdipCreateFromHDC(hdc, &graphics) != Ok) {
startup_mutex.unlock();
return null;
}
if (GdipSetSmoothingMode(graphics, SmoothingModeAntiAlias) != Ok) {
_ = GdipDeleteGraphics(graphics);
startup_mutex.unlock();
return null;
}
var pen: *GpPen = undefined;
if (GdipCreatePen1(colorrefToArgb(colorref), width, UnitPixel, &pen) != Ok) {
_ = GdipDeleteGraphics(graphics);
startup_mutex.unlock();
return null;
}
return .{ .graphics = graphics, .pen = pen };
Expand All @@ -112,6 +170,7 @@ fn beginSession(hdc: c.HDC, colorref: u32, width: f32) ?Session {
fn endSession(session: Session) void {
_ = GdipDeletePen(session.pen);
_ = GdipDeleteGraphics(session.graphics);
startup_mutex.unlock();
}

/// Draws an anti-aliased straight line. Returns false (drawing nothing)
Expand Down Expand Up @@ -174,6 +233,8 @@ pub fn drawRoundedRect(
border_colorref: u32,
border_width: f32,
) bool {
startup_mutex.lock();
defer startup_mutex.unlock();
if (!available) return false;
var graphics: *GpGraphics = undefined;
if (GdipCreateFromHDC(hdc, &graphics) != Ok) return false;
Expand Down
60 changes: 60 additions & 0 deletions graphcode-windows/src/GdiplusStartupLiveRunner.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const std = @import("std");
const GdiplusAA = @import("GdiplusAA.zig");
const c = @import("Win32.zig").c;

const class_name = std.unicode.utf8ToUtf16LeStringLiteral("GraphCodeGdiplusStartupTest");
const title = std.unicode.utf8ToUtf16LeStringLiteral("GraphCode GDI+ startup test");

const BlockingStartup = struct {
pub fn run() ?usize {
while (true) std.Thread.sleep(std.time.ns_per_s);
}
};

fn windowProc(
hwnd: c.HWND,
message: c.UINT,
wparam: c.WPARAM,
lparam: c.LPARAM,
) callconv(.winapi) c.LRESULT {
if (message == c.WM_DESTROY) {
c.PostQuitMessage(0);
return 0;
}
return c.DefWindowProcW(hwnd, message, wparam, lparam);
}

pub fn main() !void {
GdiplusAA.initWith(BlockingStartup);

const instance = c.GetModuleHandleW(null);
var klass: c.WNDCLASSW = std.mem.zeroes(c.WNDCLASSW);
klass.lpfnWndProc = &windowProc;
klass.hInstance = instance;
klass.lpszClassName = class_name.ptr;
if (c.RegisterClassW(&klass) == 0 and c.GetLastError() != c.ERROR_CLASS_ALREADY_EXISTS)
return error.WindowClassRegistrationFailed;

const hwnd = c.CreateWindowExW(
0,
class_name.ptr,
title.ptr,
c.WS_OVERLAPPEDWINDOW,
c.CW_USEDEFAULT,
c.CW_USEDEFAULT,
640,
480,
null,
null,
instance,
null,
) orelse return error.WindowCreationFailed;
_ = c.ShowWindow(hwnd, c.SW_SHOW);
_ = c.UpdateWindow(hwnd);

var message: c.MSG = undefined;
while (c.GetMessageW(&message, null, 0, 0) > 0) {
_ = c.TranslateMessage(&message);
_ = c.DispatchMessageW(&message);
}
}
Loading