@@ -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