Skip to content

Commit c91d48a

Browse files
committed
#4017 scale OpenGL viewport on MacOS with 'backingScaleFactor'
1 parent 1410094 commit c91d48a

File tree

6 files changed

+61
-37
lines changed

6 files changed

+61
-37
lines changed

Diff for: xpra/client/gl/backing.py

+37-30
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def resize_fbo(self, context, oldw : int, oldh : int, bw : int, bh : int) -> Non
420420
dy = (bh-h)-dy
421421
#re-init our OpenGL context with the new size,
422422
#but leave offscreen fbo with the old size
423-
self.gl_init(True)
423+
self.gl_init(context, True)
424424
#copy offscreen to new tmp:
425425
self.copy_fbo(w, h, sx, sy, dx, dy)
426426
#make tmp the new offscreen:
@@ -518,7 +518,7 @@ def gl_init_shaders(self) -> None:
518518
raise RuntimeError(f"OpenGL shader {name} setup failure: {err}")
519519
log("%s shader initialized", name)
520520

521-
def gl_init(self, skip_fbo:bool=False) -> None:
521+
def gl_init(self, context, skip_fbo:bool=False) -> None:
522522
#must be called within a context!
523523
#performs init if needed
524524
if not self.debug_setup:
@@ -534,8 +534,9 @@ def gl_init(self, skip_fbo:bool=False) -> None:
534534
self.gl_marker("Initializing GL context for window size %s, backing size %s, max texture size=%i",
535535
self.render_size, self.size, mt)
536536
# Initialize viewport and matrices for 2D rendering
537-
x, _, _, y = self.offsets
538-
glViewport(x, y, w, h)
537+
# default is to paint to pbo, so without any scale factor or offsets
538+
# (as those are only applied when painting the window)
539+
glViewport(0, 0, w, h)
539540
glMatrixMode(GL_PROJECTION)
540541
glLoadIdentity()
541542
glOrtho(0.0, w, h, 0.0, -1.0, 1.0)
@@ -583,7 +584,7 @@ def gl_init(self, skip_fbo:bool=False) -> None:
583584
# Bind program 0 for YUV painting by default
584585
glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, self.shaders[YUV_to_RGB_SHADER])
585586
self.gl_setup = True
586-
log("gl_init(%s) done", skip_fbo)
587+
log("gl_init(%s, %s) done", context, skip_fbo)
587588

588589
def get_init_magfilter(self) -> IntConstant:
589590
rw, rh = self.render_size
@@ -710,7 +711,7 @@ def fail(msg):
710711
glDisable(target)
711712
fire_paint_callbacks(callbacks, True)
712713
if not self.draw_needs_refresh:
713-
self.present_fbo(0, 0, bw, bh, flush)
714+
self.present_fbo(context, 0, 0, bw, bh, flush)
714715

715716
def copy_fbo(self, w : int, h : int, sx : int=0, sy : int=0, dx : int=0, dy : int=0) -> None:
716717
log("copy_fbo%s", (w, h, sx, sy, dx, dy))
@@ -742,28 +743,32 @@ def swap_fbos(self) -> None:
742743
self.textures[TEX_TMP_FBO] = tmp
743744

744745

745-
def present_fbo(self, x : int, y : int, w : int, h : int, flush=0) -> None:
746+
def present_fbo(self, context, x : int, y : int, w : int, h : int, flush=0) -> None:
746747
log("present_fbo: adding %s to pending paint list (size=%i), flush=%s, paint_screen=%s",
747748
(x, y, w, h), len(self.pending_fbo_paint), flush, self.paint_screen)
749+
if not context:
750+
raise RuntimeError("missing OpenGL paint context")
748751
self.pending_fbo_paint.append((x, y, w, h))
749752
if not self.paint_screen:
750753
return
751754
#flush>0 means we should wait for the final flush=0 paint
752755
if flush==0 or not PAINT_FLUSH:
753756
self.record_fps_event()
754-
self.managed_present_fbo()
757+
self.managed_present_fbo(context)
755758

756-
def managed_present_fbo(self) -> None:
759+
def managed_present_fbo(self, context) -> None:
760+
if not context:
761+
raise RuntimeError("missing opengl paint context")
757762
try:
758763
with paint_context_manager:
759-
self.do_present_fbo()
764+
self.do_present_fbo(context)
760765
except Exception as e:
761766
log.error("Error presenting FBO:")
762767
log.estr(e)
763768
log("Error presenting FBO", exc_info=True)
764769
self.last_present_fbo_error = str(e)
765770

766-
def do_present_fbo(self) -> None:
771+
def do_present_fbo(self, context) -> None:
767772
bw, bh = self.size
768773
ww, wh = self.render_size
769774
rect_count = len(self.pending_fbo_paint)
@@ -783,10 +788,10 @@ def do_present_fbo(self) -> None:
783788
glBindFramebuffer(GL_FRAMEBUFFER, 0)
784789
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
785790

786-
left, top, right, bottom = self.offsets
787-
788791
#viewport for clearing the whole window:
789-
glViewport(0, 0, left+ww+right, top+wh+bottom)
792+
scale = context.get_scale_factor()
793+
left, top, right, bottom = self.offsets
794+
glViewport(0, 0, int((left+ww+right)*scale), int((top+wh+bottom)*scale))
790795
if self._alpha_enabled:
791796
# transparent background:
792797
glClearColor(0.0, 0.0, 0.0, 0.0)
@@ -796,10 +801,12 @@ def do_present_fbo(self) -> None:
796801
if left or top or right or bottom:
797802
self.gl_clear_color_buffer()
798803

799-
#from now on, take the offsets into account:
800-
glViewport(left, top, ww, wh)
804+
#from now on, take the offsets and scaling into account:
805+
viewport = int(left*scale), int(top*scale), int(ww*scale), int(wh*scale)
806+
log(f"window viewport for {self.render_size=} and {self.offsets} with scale factor {scale}: {viewport}")
807+
glViewport(*viewport)
801808
target = GL_TEXTURE_RECTANGLE_ARB
802-
if ww!=bw or wh!=bh:
809+
if ww!=bw or wh!=bh or scale!=1:
803810
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
804811
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
805812

@@ -1006,7 +1013,7 @@ def set_cursor_data(self, cursor_data) -> None:
10061013
pixels = cursor_data[8]
10071014
def gl_upload_cursor(context):
10081015
if context:
1009-
self.gl_init()
1016+
self.gl_init(context)
10101017
self.upload_rgba_texture(self.textures[TEX_CURSOR], cw, ch, pixels)
10111018
self.with_gl_context(gl_upload_cursor)
10121019

@@ -1119,9 +1126,9 @@ def paint_nvdec(gl_context):
11191126
self.idle_add(self.do_paint_rgb, rgb_format, img.get_pixels(), x, y, w, h, width, height,
11201127
img.get_rowstride(), options, callbacks)
11211128

1122-
def cuda_buffer_to_pbo(self, cuda_buffer, rowstride:int, src_y:int, height:int, stream):
1129+
def cuda_buffer_to_pbo(self, gl_context, cuda_buffer, rowstride:int, src_y:int, height:int, stream):
11231130
#must be called with an active cuda context, and from the UI thread
1124-
self.gl_init()
1131+
self.gl_init(gl_context)
11251132
pbo = glGenBuffers(1)
11261133
size = rowstride*height
11271134
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo)
@@ -1169,8 +1176,8 @@ def paint_nvdec(self, gl_context, encoding, img_data, x : int, y : int, width :
11691176
strides = img.get_rowstride()
11701177
height = img.get_height()
11711178
try:
1172-
y_pbo = self.cuda_buffer_to_pbo(cuda_buffer, strides[0], 0, height, stream)
1173-
uv_pbo = self.cuda_buffer_to_pbo(cuda_buffer, strides[1], roundup(height, 2), height//2, stream)
1179+
y_pbo = self.cuda_buffer_to_pbo(gl_context, cuda_buffer, strides[0], 0, height, stream)
1180+
uv_pbo = self.cuda_buffer_to_pbo(gl_context, cuda_buffer, strides[1], roundup(height, 2), height//2, stream)
11741181
except LogicError as e:
11751182
#disable nvdec from now on:
11761183
self.nvdec_decoder = None
@@ -1203,7 +1210,7 @@ def paint_nvjpeg(self, gl_context, encoding, img_data, x : int, y : int, width :
12031210
raise ValueError(f"unexpected rgb format {rgb_format}")
12041211
#'pixels' is a cuda buffer:
12051212
cuda_buffer = img.get_pixels()
1206-
pbo = self.cuda_buffer_to_pbo(cuda_buffer, img.get_rowstride(), 0, img.get_height(), stream)
1213+
pbo = self.cuda_buffer_to_pbo(gl_context, cuda_buffer, img.get_rowstride(), 0, img.get_height(), stream)
12071214
cuda_buffer.free()
12081215

12091216
pformat = PIXEL_FORMAT_TO_CONSTANT[rgb_format]
@@ -1235,7 +1242,7 @@ def paint_nvjpeg(self, gl_context, encoding, img_data, x : int, y : int, width :
12351242
self.paint_box(encoding, x, y, width, height)
12361243
# Present update to screen
12371244
if not self.draw_needs_refresh:
1238-
self.present_fbo(x, y, width, height, options.intget("flush", 0))
1245+
self.present_fbo(gl_context, x, y, width, height, options.intget("flush", 0))
12391246
# present_fbo has reset state already
12401247
fire_paint_callbacks(callbacks)
12411248
glDeleteBuffers(1, [pbo])
@@ -1293,7 +1300,7 @@ def gl_paint_rgb(self, context, rgb_format:str, img_data,
12931300
try:
12941301
upload, img_data = self.pixels_for_upload(img_data)
12951302

1296-
self.gl_init()
1303+
self.gl_init(context)
12971304
scaling = width!=render_width or height!=render_height
12981305

12991306
#convert it to a GL constant:
@@ -1339,7 +1346,7 @@ def gl_paint_rgb(self, context, rgb_format:str, img_data,
13391346
self.paint_box(options.strget("encoding"), x, y, render_width, render_height)
13401347
# Present update to screen
13411348
if not self.draw_needs_refresh:
1342-
self.present_fbo(x, y, render_width, render_height, options.intget("flush", 0))
1349+
self.present_fbo(context, x, y, render_width, render_height, options.intget("flush", 0))
13431350
# present_fbo has reset state already
13441351
fire_paint_callbacks(callbacks)
13451352
return
@@ -1404,7 +1411,7 @@ def do_gl_paint_planar(self, context, shader, flush:int, encoding:str, img,
14041411
log("%s._do_paint_rgb(..) no OpenGL context!", self)
14051412
fire_paint_callbacks(callbacks, False, "failed to get a gl context")
14061413
return
1407-
self.gl_init()
1414+
self.gl_init(context)
14081415
scaling = enc_width!=width or enc_height!=height
14091416
self.update_planar_textures(enc_width, enc_height, img, pixel_format, scaling=scaling, pbo=options.get("pbo"))
14101417

@@ -1419,7 +1426,7 @@ def do_gl_paint_planar(self, context, shader, flush:int, encoding:str, img,
14191426
fire_paint_callbacks(callbacks, True)
14201427
# Present it on screen
14211428
if not self.draw_needs_refresh:
1422-
self.present_fbo(x, y, width, height, flush)
1429+
self.present_fbo(context, x, y, width, height, flush)
14231430
img.free()
14241431
return
14251432
except GLError as e:
@@ -1578,6 +1585,6 @@ def gl_expose_rect(self, rect=None) -> None:
15781585
rect = (0, 0, w, h)
15791586
def expose(context):
15801587
if context:
1581-
self.gl_init()
1582-
self.present_fbo(*rect)
1588+
self.gl_init(context)
1589+
self.present_fbo(context, *rect)
15831590
self.with_gl_context(expose)

Diff for: xpra/client/gl/gtk3/client_window.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def magic_key(self, *args) -> None:
8585
if self.border:
8686
self.border.toggle()
8787
if b:
88-
with b.gl_context():
89-
b.gl_init()
90-
b.present_fbo(0, 0, *b.size)
88+
with b.gl_context() as ctx:
89+
b.gl_init(ctx)
90+
b.present_fbo(ctx, 0, 0, *b.size)
9191
self.repaint(0, 0, *self._size)
9292
log("gl magic_key%s border=%s, backing=%s", args, self.border, b)
9393

Diff for: xpra/client/gl/gtk3/drawing_area.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def gl_context(self):
8585
if not gdk_window:
8686
raise RuntimeError(f"backing {b} does not have a gdk window!")
8787
self.window_context = self.context.get_paint_context(gdk_window)
88+
if not self.window_context:
89+
raise RuntimeError(f"failed to get an OpenGL window context for {gdk_window} from {self.context}")
8890
return self.window_context
8991

9092
def do_gl_show(self, rect_count) -> None:
@@ -104,6 +106,6 @@ def close_gl_config(self) -> None:
104106

105107
def draw_fbo(self, _context) -> None:
106108
w, h = self.size
107-
with self.gl_context():
108-
self.gl_init()
109-
self.present_fbo(0, 0, w, h)
109+
with self.gl_context() as ctx:
110+
self.gl_init(ctx)
111+
self.present_fbo(ctx, 0, 0, w, h)

Diff for: xpra/platform/darwin/gl_context.py

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class AGLContext:
6868

6969
def __init__(self, alpha=True):
7070
self.alpha = alpha
71+
self.scale_factor = 1.0
7172
self.gl_context : NSOpenGLContext | None = None
7273
self.nsview_ptr : int = 0
7374
self.window_context : AGLWindowContext | None = None
@@ -165,11 +166,15 @@ def get_paint_context(self, gdk_window) -> AGLWindowContext:
165166
self.gl_context.setValues_forParameter_([0], NSOpenGLCPSurfaceOpacity)
166167
enable_transparency(gdk_window)
167168
self.window_context = AGLWindowContext(self.gl_context, nsview)
169+
self.scale_factor = nsview.screen().backingScaleFactor()
168170
return self.window_context
169171

170172
def __del__(self):
171173
self.destroy()
172174

175+
def get_scale_factor(self) -> float:
176+
return self.scale_factor
177+
173178
def destroy(self) -> None:
174179
c = self.window_context
175180
log("AGLContext.destroy() window_context=%s", c)

Diff for: xpra/platform/posix/gl_context.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from OpenGL import GLX
99
from OpenGL.GL import GL_VENDOR, GL_RENDERER, glGetString
1010

11-
from xpra.util.env import envbool
11+
from xpra.util.env import envbool, envfloat
1212
from xpra.client.gl.check import check_PyOpenGL_support
1313
from xpra.x11.bindings.display_source import get_display_ptr #@UnresolvedImport
1414
from xpra.gtk.error import xsync
@@ -19,6 +19,9 @@
1919

2020

2121
DOUBLE_BUFFERED = envbool("XPRA_OPENGL_DOUBLE_BUFFERED", True)
22+
SCALE_FACTOR = envfloat("XPRA_OPENGL_SCALE_FACTOR", 1)
23+
if SCALE_FACTOR<=0 or SCALE_FACTOR>10:
24+
raise ValueError(f"invalid scale factor {SCALE_FACTOR}")
2225

2326

2427
GLX_ATTRIBUTES : dict[Any,str] = {
@@ -92,6 +95,9 @@ def swap_buffers(self) -> None:
9295
assert self.valid, "GLX window context is no longer valid"
9396
GLX.glXSwapBuffers(self.xdisplay, self.xid)
9497

98+
def get_scale_factor(self) -> float:
99+
return SCALE_FACTOR
100+
95101
def __repr__(self):
96102
return "GLXWindowContext(%#x)" % self.xid
97103

Diff for: xpra/platform/win32/gl_context.py

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
def DefWndProc(hwnd, msg, wParam, lParam):
4242
return DefWindowProcA(hwnd, msg, wParam, lParam)
4343

44+
4445
class WGLWindowContext:
4546

4647
def __init__(self, hwnd:int, hdc:int, context):
@@ -77,6 +78,9 @@ def swap_buffers(self):
7778
log("swap_buffers: calling SwapBuffers(%#x)", self.paint_hdc)
7879
SwapBuffers(self.paint_hdc)
7980

81+
def get_scale_factor(self) -> float:
82+
return 1
83+
8084
def __repr__(self):
8185
return "WGLWindowContext(%#x)" % self.hwnd
8286

0 commit comments

Comments
 (0)