Skip to content

Commit fbf79e7

Browse files
seto77claude
andcommitted
Fix OpenGL rendering corruption on Windows on ARM (x64 emulation)
The OpenGL driver on Windows on ARM is GLOn12 (Mesa d3d12 gallium). Its DXGI swapchain, once created at a small size during early empty renders, fails to grow on a later height-only resize, leaving fixed-size GLControls (e.g. FormMain.glControlAxes) with a black lower half and overflowing content. Defer the first default-framebuffer present until the first real content render so the swapchain is created at the final size (ARM only). Also base viewport/projection/FBO sizing on the real GLFW framebuffer size instead of WinForms ClientSize, disable MSAA on ARM, and force a dual-MoveWindow resize. Add an env-gated diagnostic facility (RECIPRO_GLDIAG=1 = logs, =bands = color-band swapchain-size test); no-op and no log file when unset. Set IndexControl/SizeControl AutoScaleMode to Dpi with 96,96 dimensions for high-DPI correctness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a5524a0 commit fbf79e7

9 files changed

Lines changed: 513 additions & 31 deletions

File tree

Crystallography.Controls/Numeric/IndexControl.Designer.cs

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Crystallography.Controls/Numeric/SizeControl.Designer.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Crystallography.Controls/Numeric/SizeControl.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@
395395
<value>True</value>
396396
</metadata>
397397
<data name="$this.AutoScaleDimensions" type="System.Drawing.SizeF, System.Drawing">
398-
<value>7, 15</value>
398+
<value>96, 96</value>
399399
</data>
400400
<data name="$this.AutoSize" type="System.Boolean, mscorlib">
401401
<value>True</value>

Crystallography.OpenGL/GLControlAlpha.cs

Lines changed: 198 additions & 24 deletions
Large diffs are not rendered by default.

Crystallography.OpenGL/OpenTK.GLControl/GLControl.cs

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public class GLControl : Control
2727
/// </summary>
2828
private NativeWindow? _nativeWindow = null;
2929

30+
// 260529Cl 追加: GLFW 子ウィンドウの Win32 HWND をキャッシュする。
31+
// Windows on ARM (x64 エミュ) では GLFW 経由のリサイズが親と
32+
// 一致しないことがあるため、Win32 API で直接サイズ強制するのに使う。
33+
private IntPtr _nativeHwnd = IntPtr.Zero;
34+
3035
// Indicates that OnResize was called before OnHandleCreated.
3136
// To avoid issues with missing OpenGL contexts, we suppress
3237
// the premature Resize event and raise it as soon as the handle
@@ -262,15 +267,19 @@ protected override void OnHandleCreated(EventArgs e)
262267
{
263268
// We don't convert the GLControlSettings to NativeWindowSettings here as that would call GLFW.
264269
// And this function will be created in design mode.
270+
GLDebugLog.Log(LogName, "OnHandleCreated/enter", SnapshotSize());
265271
CreateNativeWindow(_glControlSettings);
272+
GLDebugLog.Log(LogName, "OnHandleCreated/afterCreate", SnapshotSize());
266273

267274
base.OnHandleCreated(e);
268275

269276
if (_resizeEventSuppressed)
270277
{
278+
GLDebugLog.Log(LogName, "OnHandleCreated/suppressedResize", SnapshotSize());
271279
OnResize(EventArgs.Empty);
272280
_resizeEventSuppressed = false;
273281
}
282+
GLDebugLog.Log(LogName, "OnHandleCreated/exit", SnapshotSize());
274283

275284
if (IsDesignMode)
276285
{
@@ -324,17 +333,92 @@ private void CreateNativeWindow(GLControlSettings glControlSettings)
324333
}
325334

326335
NativeWindowSettings nativeWindowSettings = glControlSettings.ToNativeWindowSettings();
336+
GLDebugLog.Log(LogName, "CreateNW/settings", $"settings.ClientSize=({nativeWindowSettings.ClientSize.X},{nativeWindowSettings.ClientSize.Y}) StartVisible={nativeWindowSettings.StartVisible}");
327337

328338
_nativeWindow = new NativeWindow(nativeWindowSettings);
329339
_nativeWindow.FocusedChanged += OnNativeWindowFocused;
340+
_nativeWindow.FramebufferResize += OnNativeFramebufferResize;
341+
GLDebugLog.Log(LogName, "CreateNW/nwCreated", SnapshotSize());
330342

331343
NonportableReparent(_nativeWindow);
344+
GLDebugLog.Log(LogName, "CreateNW/reparented", SnapshotSize());
332345

333346
// Force the newly child-ified GLFW window to be resized to fit this control.
334347
ResizeNativeWindow();
348+
GLDebugLog.Log(LogName, "CreateNW/afterResize", SnapshotSize());
335349

336350
// And now show the child window, since it hasn't been made visible yet.
337351
_nativeWindow.IsVisible = true;
352+
GLDebugLog.Log(LogName, "CreateNW/afterShow", SnapshotSize());
353+
354+
// 260529Cl 追加: 一部環境 (Windows on ARM x64 エミュ等) で、hidden 状態の MoveWindow が
355+
// OpenGL の back buffer 再確保まで波及せず、GL 既定の初期サイズ (NativeWindowSettings 既定 = 800×600 等) で
356+
// back buffer が固定されてしまう現象が確認されている。Anchor=Top|Right などサイズ追随しない GLControl だと
357+
// 初回ハンドル作成時の ResizeNativeWindow 以降一切 WM_SIZE が来ないため、この初期不整合がそのまま固定化し、
358+
// アスペクト比のズレや黒帯やはみ出しとして可視化する。ここで明示的に "違うサイズ → 目標サイズ" を 2 段送って
359+
// WM_SIZE を確実に発火させ、表示済み window の back buffer を実寸に同期させる。
360+
// (同サイズ MoveWindow は Windows が WM_SIZE を抑制するため、必ず一旦別サイズを経由する)
361+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _nativeHwnd != IntPtr.Zero)
362+
{
363+
int w = Math.Max(1, Width);
364+
int h = Math.Max(1, Height);
365+
Win32.MoveWindow(_nativeHwnd, 0, 0, Math.Max(1, w - 1), Math.Max(1, h - 1), false);
366+
GLDebugLog.Log(LogName, "CreateNW/forced1", SnapshotSize());
367+
Win32.MoveWindow(_nativeHwnd, 0, 0, w, h, false);
368+
GLDebugLog.Log(LogName, "CreateNW/forced2", SnapshotSize());
369+
}
370+
}
371+
372+
// 260529Cl 追加: GLFW の framebuffer_size_callback が発火した時のログ。
373+
// この値が現在の WinForms Width/Height と一致しているかを追跡することで、ARM 環境での同期不整合を可視化する。
374+
private void OnNativeFramebufferResize(OpenTK.Windowing.Common.FramebufferResizeEventArgs e)
375+
{
376+
GLDebugLog.Log(LogName, "FramebufferResize", $"event=({e.Width},{e.Height}) | {SnapshotSize()}");
377+
}
378+
379+
// 260529Cl 追加: ログ用の識別名。GLControlAlpha (UserControl) が親で Name に "glControlReciProObjects" 等を持つので、
380+
// Parent.Name を優先することで FormRotation から振った名前を捕まえる。Parent がいなければ自身の Name にフォールバック。
381+
// 内部 glControl 自身の Name (= "glControl{N}") も message 側で残せるよう、こちらでは Parent.Name のみ返す。
382+
private string LogName => Parent?.Name ?? Name;
383+
384+
// 260529Cl 追加: 各時点の WinForms / GLFW / HWND のサイズを 1 行で記録する診断ヘルパー。
385+
private string SnapshotSize()
386+
{
387+
if (!GLDebugLog.Enabled) return ""; // 260529Cl: 診断無効時は Win32 呼び出しを省く (通常起動のホットパス保護)
388+
string fb = "FB=null", ncSize = "", ncClient = "";
389+
if (_nativeWindow != null)
390+
{
391+
var fbs = _nativeWindow.FramebufferSize;
392+
fb = $"FB=({fbs.X},{fbs.Y})";
393+
var ns = _nativeWindow.Size;
394+
ncSize = $" NW.Size=({ns.X},{ns.Y})";
395+
var cs = _nativeWindow.ClientSize;
396+
ncClient = $" NW.CSize=({cs.X},{cs.Y})";
397+
}
398+
string rect = "rect=N/A";
399+
string pos = "pos=N/A";
400+
if (_nativeHwnd != IntPtr.Zero)
401+
{
402+
if (Win32.GetClientRect(_nativeHwnd, out var rc))
403+
rect = $"rect=({rc.Width},{rc.Height})";
404+
// 260529Cl 追加: GLFW HWND の screen 上の絶対位置と、親 HWND との相対位置を記録
405+
if (Win32.GetWindowRect(_nativeHwnd, out var wr))
406+
{
407+
var parentHwnd = Win32.GetParent(_nativeHwnd);
408+
if (parentHwnd != IntPtr.Zero && Win32.GetWindowRect(parentHwnd, out var pr))
409+
{
410+
// 親 HWND の WindowRect は外枠なので、親の ClientRect 内オフセットを正確にはこれだけでは出せないが、
411+
// child の Left/Top と parent の Left/Top の差が child の screen 内 vs parent screen 内位置差。
412+
// child は SetParent で WS_CHILD になっているので、その screen 位置 = 親の screen 位置 + child の親内位置 (おおむね)
413+
pos = $"absPos=({wr.Left},{wr.Top}) deltaFromParent=({wr.Left - pr.Left},{wr.Top - pr.Top})";
414+
}
415+
else
416+
{
417+
pos = $"absPos=({wr.Left},{wr.Top})";
418+
}
419+
}
420+
}
421+
return $"[inner={Name}] WinForms=({Width},{Height}) {fb}{ncSize}{ncClient} HWND-{rect} {pos} DPI={DeviceDpi}";
338422
}
339423

340424
/// <summary>
@@ -426,6 +510,7 @@ private unsafe void NonportableReparent(NativeWindow nativeWindow)
426510
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
427511
{
428512
IntPtr hWnd = GLFW.GetWin32Window(nativeWindow.WindowPtr);
513+
_nativeHwnd = hWnd; // 260529Cl 追加: 後段 Win32 リサイズで使うため HWND を保持
429514

430515
// Reparent the real HWND under this control.
431516
Win32.SetParent(hWnd, Handle);
@@ -548,6 +633,7 @@ private void DestroyNativeWindow()
548633
_nativeWindow.Dispose();
549634
_nativeWindow = null!;
550635
}
636+
_nativeHwnd = IntPtr.Zero; // 260529Cl 追加: HWND キャッシュも破棄
551637
}
552638

553639
/// <summary>
@@ -612,9 +698,12 @@ protected override void OnResize(EventArgs e)
612698
if (!IsHandleCreated)
613699
{
614700
_resizeEventSuppressed = true;
701+
GLDebugLog.Log(LogName, "OnResize/suppressed", $"WinForms=({Width},{Height})");
615702
return;
616703
}
617704

705+
GLDebugLog.Log(LogName, "OnResize/enter", SnapshotSize());
706+
618707
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
619708
{
620709
BeginInvoke(new Action(ResizeNativeWindow)); // Need the native window to resize first otherwise our control will be in the wrong place.
@@ -624,6 +713,7 @@ protected override void OnResize(EventArgs e)
624713
ResizeNativeWindow();
625714
}
626715

716+
GLDebugLog.Log(LogName, "OnResize/exit", SnapshotSize());
627717
base.OnResize(e);
628718
}
629719

@@ -635,7 +725,68 @@ private void ResizeNativeWindow()
635725

636726
if (_nativeWindow != null)
637727
{
638-
_nativeWindow.ClientRectangle = new Box2i(0, 0, Width, Height);
728+
// 260529Cl 修正: Windows では GLFW の glfwSetWindowSize 経由 (_nativeWindow.ClientRectangle) を使うと、
729+
// PerMonitorV2 DPI 認識アプリ + Windows on ARM (x64 エミュ) の組合せで DPI 計算が食い違い、
730+
// HWND と back buffer が親 WinForms と異なる物理ピクセルサイズになる。
731+
// Win32.MoveWindow で物理ピクセル単位の指定にすると、WM_SIZE が GLFW の wndproc に同期で届き
732+
// _nativeWindow.FramebufferSize も同じ値に更新される。
733+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _nativeHwnd != IntPtr.Zero)
734+
{
735+
// 260529Cl 追加: Windows on ARM (x64 エミュ) では「片方の寸法だけ」が変化する WM_SIZE では
736+
// OpenGL の back buffer が正しく再確保されない症状が確認された (例: FormMain の glControlAxes は
737+
// Dock=Top で width のみ→height のみの 2 段 resize になり、片次元のみの WM_SIZE で driver が back buffer を
738+
// 旧サイズのまま残し、Viewport だけ新サイズで描画されて「上にはみ出す」描画になる)。
739+
// 必ず両次元が変化する中間サイズを経由する dual-MoveWindow にすることで、driver に確実に
740+
// 両次元の再確保を要求する。同サイズ MoveWindow は Windows が WM_SIZE を抑制するため、わざと
741+
// (w-1, h-1) を経由する。
742+
int w = Math.Max(1, Width);
743+
int h = Math.Max(1, Height);
744+
Win32.MoveWindow(_nativeHwnd, 0, 0, Math.Max(1, w - 1), Math.Max(1, h - 1), false);
745+
Win32.MoveWindow(_nativeHwnd, 0, 0, w, h, false);
746+
}
747+
else
748+
{
749+
_nativeWindow.ClientRectangle = new Box2i(0, 0, Width, Height);
750+
}
751+
}
752+
}
753+
754+
// 260529Cl 追加: GLFW が管理している実 framebuffer (OpenGL back buffer) のピクセルサイズ。
755+
// GLFW は wndproc で WM_SIZE を受信した時に framebuffer_size_callback を同期発火し、
756+
// この値を更新する。Win32.GetClientRect を直接呼ぶより、GLFW が認識している back buffer
757+
// サイズと完全一致するため、GL.Viewport のサイズ源として用いる方が安全。
758+
/// <summary>
759+
/// Pixel size of the actual GL back buffer as known to GLFW.
760+
/// Returns <see cref="System.Drawing.Size.Empty"/> when the native window has not been created.
761+
/// </summary>
762+
[Browsable(false)]
763+
public System.Drawing.Size FramebufferPixelSize
764+
{
765+
get
766+
{
767+
if (_nativeWindow == null)
768+
return System.Drawing.Size.Empty;
769+
var sz = _nativeWindow.FramebufferSize;
770+
return new System.Drawing.Size(sz.X, sz.Y);
771+
}
772+
}
773+
774+
// 260529Cl 追加 (診断用): GLFW に毎回 glfwGetFramebufferSize を問い合わせて取得する live 値。
775+
// FramebufferPixelSize は wndproc 同期で更新される cached 値 (_nativeWindow.FramebufferSize) を返すが、
776+
// Windows on ARM (x64 エミュ) で「cached 値は正しいが driver 内部の drawable/swapchain は旧サイズ」という
777+
// 不一致が疑われるため、cached と live の差を切り分けるための観測専用プロパティ。
778+
// ARM の GL 不具合再発時の診断用に温存 (RECIPRO_GLDIAG 診断ツールの一部)。詳細: ReciPro_WindowsOnARM_OpenGL調査.md
779+
/// <summary>GL back buffer size queried live from GLFW (glfwGetFramebufferSize); diagnostic counterpart of <see cref="FramebufferPixelSize"/>.</summary>
780+
[Browsable(false)]
781+
public System.Drawing.Size FramebufferPixelSizeLive
782+
{
783+
get
784+
{
785+
if (_nativeWindow == null)
786+
return System.Drawing.Size.Empty;
787+
int w, h;
788+
unsafe { GLFW.GetFramebufferSize(_nativeWindow.WindowPtr, out w, out h); }
789+
return new System.Drawing.Size(w, h);
639790
}
640791
}
641792

0 commit comments

Comments
 (0)