mirror of
https://git.vladimir.cc/vladimir/lazopenglcontextex.git
synced 2026-08-25 17:37:31 +00:00
LOpenGLPixelScale в Qt-бэкенде (QLCLGLWidget_devicePixelRatioF), на прочих бэкендах возвращает 1.0. Нужен коду с явными glViewport и текстурами в физических пикселях. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
381 lines
14 KiB
ObjectPascal
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.
|