Files
lazopenglcontextex/glqtnativecontext.pas
T
ew8bakandClaude Fable 5 dd9717b982 feat: TCustomOpenGLControl.PixelScale — devicePixelRatio для HiDPI
LOpenGLPixelScale в Qt-бэкенде (QLCLGLWidget_devicePixelRatioF),
на прочих бэкендах возвращает 1.0. Нужен коду с явными glViewport
и текстурами в физических пикселях.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:12:11 +03:00

381 lines
14 KiB
ObjectPascal

{
*****************************************************************************
See the file COPYING.modifiedLGPL.txt, included in this distribution,
for details about the license.
*****************************************************************************
Qt5/Qt6 OpenGL backend built on a real QOpenGLWidget (libqlclglwidget)
instead of GLX. Qt creates and manages the GL context through the
platform-appropriate API (EGL on Wayland, GLX on X11, WGL on Windows,
CGL on macOS), so this backend works on every platform the Qt widgetset
runs on — including Wayland sessions, where the classic GLX backend
(GLQTContext) cannot work at all.
Why a helper library: libQt6Pas's QLCLOpenGLWidget is NOT a QOpenGLWidget —
it is a plain QWidget with WA_NativeWindow/paintEngine()=nullptr meant as a
target for an external GLX context, X11 only. The real QOpenGLWidget
subclass with a Pascal paintGL hook lives in the small companion library
libqlclglwidget (csrc/qlclglwidget.cpp), loaded at runtime from the
application directory or the system library path.
How it works:
- Qt makes the widget's context current and binds its backing FBO before
invoking paintGL(); we deliver LM_PAINT from there, so all rendering in
OnPaint just works. MakeCurrent during paint is a no-op.
- MakeCurrent/ReleaseContext outside of paint map to
QOpenGLWidget::makeCurrent/doneCurrent.
- SwapBuffers is a no-op during paint (Qt composites the FBO itself after
paintGL returns); outside of paint it schedules a repaint.
Notes:
- Contexts of all QOpenGLWidgets inside the same top-level window are
automatically shared by Qt; the SharedControl property is ignored.
- QOpenGLWidget renders into an FBO: do not call glBindFramebuffer(0)
from user code.
- On HiDPI (devicePixelRatio > 1) the backing FBO is larger than the
logical widget size; LOpenGLViewport (AutoResizeViewport) scales by the
devicePixelRatio, but explicit glViewport calls in user code must scale
themselves.
}
unit GLQtNativeContext;
{$mode objfpc}{$H+}
{$PACKRECORDS C}
interface
{$IF NOT (DEFINED(LCLQt5) OR DEFINED(LCLQt6))}
{$ERROR GLQtNativeContext supports only the Qt5/Qt6 widgetsets}
{$ENDIF}
uses
Classes, SysUtils, Controls, LCLProc, LCLType, gl, dynlibs,
InterfaceBase, WSLCLClasses,
// Bindings
{$IFDEF LCLQt5}qt5,{$ENDIF}
{$IFDEF LCLQt6}qt6,{$ENDIF}
qtwidgets, qtint;
procedure LOpenGLViewport(Handle: HWND; Left, Top, Width, Height: integer);
function LOpenGLPixelScale(Handle: HWND): Double;
procedure LOpenGLSwapBuffers(Handle: HWND);
function LOpenGLMakeCurrent(Handle: HWND): boolean;
function LOpenGLReleaseContext(Handle: HWND): boolean;
function LOpenGLCreateContext(AWinControl: TWinControl;
WSPrivate: TWSPrivateClass; SharedControl: TWinControl;
DoubleBuffered, RGBA: boolean;
const RedBits, GreenBits, BlueBits, MajorVersion, MinorVersion,
MultiSampling, AlphaBits, DepthBits, StencilBits, AUXBuffers: Cardinal;
const AParams: TCreateParams): HWND;
procedure LOpenGLDestroyContextInfo(AWinControl: TWinControl);
implementation
uses LMessages, Forms;
{ ---------------------------------------------------------------------------
libqlclglwidget, loaded at runtime
--------------------------------------------------------------------------- }
const
GLWidgetLibName =
{$IFDEF WINDOWS}'qlclglwidget.dll'{$ELSE}
{$IFDEF DARWIN}'libqlclglwidget.dylib'{$ELSE}
'libqlclglwidget.so'{$ENDIF}{$ENDIF};
type
// an FPC method pointer passed by value; matches the C-side
// QGLOverrideHook {void *func; void *data;}
TGLWidgetPaintHook = procedure of object; cdecl;
TQLCLGLWidget_Create = function(parent: QWidgetH; flags: Cardinal): QWidgetH; cdecl;
TQLCLGLWidget_Method = procedure(handle: QWidgetH); cdecl;
TQLCLGLWidget_override_paintGL = procedure(handle: QWidgetH; hook: TGLWidgetPaintHook); cdecl;
TQLCLGLWidget_isValid = function(handle: QWidgetH): ByteBool; cdecl;
TQLCLGLWidget_setFormat = procedure(handle: QWidgetH; fmt: QSurfaceFormatH); cdecl;
TQLCLGLWidget_setForceOpaque = procedure(handle: QWidgetH; enable: ByteBool); cdecl;
TQLCLGLWidget_defaultFBO = function(handle: QWidgetH): Cardinal; cdecl;
TQLCLGLWidget_dprF = function(handle: QWidgetH): Double; cdecl;
var
GLWidgetLibTried: boolean = false;
GLWidgetLib: TLibHandle = NilHandle;
QLCLGLWidget_Create: TQLCLGLWidget_Create = nil;
QLCLGLWidget_override_paintGL: TQLCLGLWidget_override_paintGL = nil;
QLCLGLWidget_makeCurrent: TQLCLGLWidget_Method = nil;
QLCLGLWidget_doneCurrent: TQLCLGLWidget_Method = nil;
QLCLGLWidget_isValid: TQLCLGLWidget_isValid = nil;
QLCLGLWidget_setFormat: TQLCLGLWidget_setFormat = nil;
QLCLGLWidget_setForceOpaque: TQLCLGLWidget_setForceOpaque = nil;
QLCLGLWidget_defaultFramebufferObject: TQLCLGLWidget_defaultFBO = nil;
QLCLGLWidget_devicePixelRatioF: TQLCLGLWidget_dprF = nil;
function LoadGLWidgetLib: boolean;
function TryLoad(const AName: string): boolean;
begin
GLWidgetLib := LoadLibrary(AName);
Result := GLWidgetLib <> NilHandle;
end;
function Need(const AName: string): Pointer;
begin
Result := GetProcedureAddress(GLWidgetLib, AName);
if Result = nil then
raise Exception.CreateFmt('%s: missing symbol %s', [GLWidgetLibName, AName]);
end;
begin
if not GLWidgetLibTried then begin
GLWidgetLibTried := true;
// next to the executable first, then the system library path
if TryLoad(ExtractFilePath(ParamStr(0)) + GLWidgetLibName)
or TryLoad(GLWidgetLibName) then begin
Pointer(QLCLGLWidget_Create) := Need('QLCLGLWidget_Create');
Pointer(QLCLGLWidget_override_paintGL) := Need('QLCLGLWidget_override_paintGL');
Pointer(QLCLGLWidget_makeCurrent) := Need('QLCLGLWidget_makeCurrent');
Pointer(QLCLGLWidget_doneCurrent) := Need('QLCLGLWidget_doneCurrent');
Pointer(QLCLGLWidget_isValid) := Need('QLCLGLWidget_isValid');
Pointer(QLCLGLWidget_setFormat) := Need('QLCLGLWidget_setFormat');
Pointer(QLCLGLWidget_setForceOpaque) := Need('QLCLGLWidget_setForceOpaque');
Pointer(QLCLGLWidget_defaultFramebufferObject) := Need('QLCLGLWidget_defaultFramebufferObject');
Pointer(QLCLGLWidget_devicePixelRatioF) := Need('QLCLGLWidget_devicePixelRatioF');
end;
end;
Result := GLWidgetLib <> NilHandle;
end;
type
{ TQtGLWidget }
TQtGLWidget = class(TQtWidget)
protected
function CreateWidget(const Params: TCreateParams): QWidgetH; override;
procedure paintGL(); cdecl; virtual;
public
InPaintGL: boolean;
function GetContainerWidget: QWidgetH; override;
procedure AttachEvents; override;
procedure DetachEvents; override;
procedure SlotPaintBg({%H-}Sender: QObjectH; {%H-}Event: QEventH); cdecl; override;
procedure SlotPaint({%H-}Sender: QObjectH; {%H-}Event: QEventH); cdecl; override;
end;
{ TQtGLWidget }
function TQtGLWidget.CreateWidget(const Params: TCreateParams): QWidgetH;
var
Parent: QWidgetH;
begin
if Params.WndParent <> 0 then
Parent := TQtWidget(Params.WndParent).GetContainerWidget
else
Parent := nil;
Widget := QLCLGLWidget_Create(Parent, 0);
Result := Widget;
end;
function TQtGLWidget.GetContainerWidget: QWidgetH;
begin
Result := Widget;
end;
procedure TQtGLWidget.AttachEvents;
begin
QLCLGLWidget_override_paintGL(Widget, @paintGL);
inherited AttachEvents;
end;
procedure TQtGLWidget.DetachEvents;
var
NilHook: TGLWidgetPaintHook;
begin
inherited DetachEvents;
TMethod(NilHook).Code := nil;
TMethod(NilHook).Data := nil;
QLCLGLWidget_override_paintGL(Widget, NilHook);
end;
procedure TQtGLWidget.SlotPaintBg(Sender: QObjectH; Event: QEventH); cdecl;
begin
// QOpenGLWidget paints itself through paintGL
end;
procedure TQtGLWidget.SlotPaint(Sender: QObjectH; Event: QEventH); cdecl;
begin
// QOpenGLWidget paints itself through paintGL
end;
procedure TQtGLWidget.paintGL(); cdecl;
var
Msg: TLMPaint;
AStruct: PPaintStruct;
B: Boolean;
begin
// Qt has already made the widget's context current and bound its FBO
if not (CanSendLCLMessage and (LCLObject is TWinControl)) then begin
DebugLn('TQtGLWidget.paintGL error CanSendLCLMessage=',dbgs(CanSendLCLMessage),
' LCLObject=',dbgsName(LCLObject));
exit;
end;
InPaintGL := true;
try
FillChar(Msg{%H-}, SizeOf(Msg), #0);
Msg.Msg := LM_PAINT;
New(AStruct);
try
try
FillChar(AStruct^, SizeOf(TPaintStruct), 0);
QWidget_rect(Widget, @AStruct^.rcPaint);
AStruct^.hdc := PtrUInt(Widget);
Msg.PaintStruct := AStruct;
Msg.DC := AStruct^.hdc;
LCLObject.WindowProc(TLMessage(Msg));
finally
Dispose(AStruct);
end;
except
// prevent recursive repainting !
B := QtWidgetSet.IsValidHandle(HWND(Self));
if B then
QWidget_setUpdatesEnabled(Widget, False);
try
Application.HandleException(nil);
finally
if B and Assigned(Application) and not Application.Terminated then
QWidget_setUpdatesEnabled(Widget, True);
end;
end;
finally
InPaintGL := false;
end;
end;
// devicePixelRatio виджета (1.0 если недоступен) — для явных glViewport
// в пользовательском коде и текстур в физических пикселях.
function LOpenGLPixelScale(Handle: HWND): Double;
var
Widget: TQtGLWidget;
begin
Result := 1.0;
if (Handle <> 0) and Assigned(QLCLGLWidget_devicePixelRatioF) then begin
Widget := TQtGLWidget(Handle);
Result := QLCLGLWidget_devicePixelRatioF(Widget.Widget);
if Result <= 0 then Result := 1.0;
end;
end;
procedure LOpenGLViewport(Handle: HWND; Left, Top, Width, Height: integer);
var
Dpr: Double;
begin
Dpr := LOpenGLPixelScale(Handle);
glViewport(Round(Left*Dpr), Round(Top*Dpr), Round(Width*Dpr), Round(Height*Dpr));
end;
procedure LOpenGLSwapBuffers(Handle: HWND);
var
Widget: TQtGLWidget;
begin
if Handle=0 then
RaiseGDBException('LOpenGLSwapBuffers Handle=0');
Widget := TQtGLWidget(Handle);
// during paintGL Qt swaps/composites the FBO itself after the handler
// returns; outside of paint the best we can do is schedule a repaint
if not Widget.InPaintGL then
QWidget_update(Widget.Widget);
end;
function LOpenGLMakeCurrent(Handle: HWND): boolean;
var
Widget: TQtGLWidget;
begin
Result := false;
if Handle=0 then
RaiseGDBException('LOpenGLMakeCurrent Handle=0');
Widget := TQtGLWidget(Handle);
if Widget.InPaintGL then
exit(true); // Qt already made the context current for paintGL
// no context yet before the widget was first realized/shown
if not QLCLGLWidget_isValid(Widget.Widget) then
exit;
QLCLGLWidget_makeCurrent(Widget.Widget);
Result := true;
end;
function LOpenGLReleaseContext(Handle: HWND): boolean;
var
Widget: TQtGLWidget;
begin
Result := false;
if Handle=0 then
RaiseGDBException('LOpenGLReleaseContext Handle=0');
Widget := TQtGLWidget(Handle);
if Widget.InPaintGL then exit; // never unbind Qt's own paint context
QLCLGLWidget_doneCurrent(Widget.Widget);
Result := true;
end;
function LOpenGLCreateContext(AWinControl: TWinControl;
WSPrivate: TWSPrivateClass; SharedControl: TWinControl;
DoubleBuffered, RGBA: boolean;
const RedBits, GreenBits, BlueBits, MajorVersion, MinorVersion,
MultiSampling, AlphaBits, DepthBits, StencilBits, AUXBuffers: Cardinal;
const AParams: TCreateParams): HWND;
var
NewQtWidget: TQtGLWidget;
AFormat: QSurfaceFormatH;
begin
if WSPrivate=nil then ;
if SharedControl<>nil then ; // Qt shares QOpenGLWidget contexts per top-level window
if AUXBuffers>0 then ; // not supported by QSurfaceFormat
if not LoadGLWidgetLib then
raise Exception.Create(GLWidgetLibName+' not found (looked next to the '
+'executable and in the system library path). Build it with '
+'"make -C csrc" from the LazOpenGLContextEx package.');
NewQtWidget := TQtGLWidget.Create(AWinControl, AParams);
NewQtWidget.HasPaint := false;
// must be set before the widget is first shown
AFormat := QSurfaceFormat_Create();
try
QSurfaceFormat_setRenderableType(AFormat, QSurfaceFormatRenderableTypeOpenGL);
if MajorVersion > 0 then begin
QSurfaceFormat_setMajorVersion(AFormat, MajorVersion);
QSurfaceFormat_setMinorVersion(AFormat, MinorVersion);
end;
if DoubleBuffered then
QSurfaceFormat_setSwapBehavior(AFormat, QSurfaceSwapBehaviorDoubleBuffer);
if RGBA then begin
QSurfaceFormat_setRedBufferSize(AFormat, RedBits);
QSurfaceFormat_setGreenBufferSize(AFormat, GreenBits);
QSurfaceFormat_setBlueBufferSize(AFormat, BlueBits);
end;
if AlphaBits > 0 then
QSurfaceFormat_setAlphaBufferSize(AFormat, AlphaBits);
QSurfaceFormat_setDepthBufferSize(AFormat, DepthBits);
QSurfaceFormat_setStencilBufferSize(AFormat, StencilBits);
if MultiSampling > 1 then
QSurfaceFormat_setSamples(AFormat, MultiSampling);
QLCLGLWidget_setFormat(NewQtWidget.Widget, AFormat);
finally
QSurfaceFormat_Destroy(AFormat);
end;
// legacy GL code leaves arbitrary alpha in the FBO which would make the
// widget translucent when Qt composites it; force alpha to 1 unless the
// control explicitly asked for an alpha channel
QLCLGLWidget_setForceOpaque(NewQtWidget.Widget, AlphaBits = 0);
NewQtWidget.AttachEvents;
Result := HWND(NewQtWidget);
end;
procedure LOpenGLDestroyContextInfo(AWinControl: TWinControl);
begin
if not AWinControl.HandleAllocated then exit;
// the QOpenGLWidget owns its context; it dies with the widget
end;
end.