diff --git a/xpra/client/gl/backing.py b/xpra/client/gl/backing.py index 4305224fd5..e0c3ab1bc7 100644 --- a/xpra/client/gl/backing.py +++ b/xpra/client/gl/backing.py @@ -420,7 +420,7 @@ def resize_fbo(self, context, oldw : int, oldh : int, bw : int, bh : int) -> Non dy = (bh-h)-dy #re-init our OpenGL context with the new size, #but leave offscreen fbo with the old size - self.gl_init(True) + self.gl_init(context, True) #copy offscreen to new tmp: self.copy_fbo(w, h, sx, sy, dx, dy) #make tmp the new offscreen: @@ -518,7 +518,7 @@ def gl_init_shaders(self) -> None: raise RuntimeError(f"OpenGL shader {name} setup failure: {err}") log("%s shader initialized", name) - def gl_init(self, skip_fbo:bool=False) -> None: + def gl_init(self, context, skip_fbo:bool=False) -> None: #must be called within a context! #performs init if needed if not self.debug_setup: @@ -534,8 +534,9 @@ def gl_init(self, skip_fbo:bool=False) -> None: self.gl_marker("Initializing GL context for window size %s, backing size %s, max texture size=%i", self.render_size, self.size, mt) # Initialize viewport and matrices for 2D rendering - x, _, _, y = self.offsets - glViewport(x, y, w, h) + # default is to paint to pbo, so without any scale factor or offsets + # (as those are only applied when painting the window) + glViewport(0, 0, w, h) glMatrixMode(GL_PROJECTION) glLoadIdentity() glOrtho(0.0, w, h, 0.0, -1.0, 1.0) @@ -583,7 +584,7 @@ def gl_init(self, skip_fbo:bool=False) -> None: # Bind program 0 for YUV painting by default glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, self.shaders[YUV_to_RGB_SHADER]) self.gl_setup = True - log("gl_init(%s) done", skip_fbo) + log("gl_init(%s, %s) done", context, skip_fbo) def get_init_magfilter(self) -> IntConstant: rw, rh = self.render_size @@ -710,7 +711,7 @@ def fail(msg): glDisable(target) fire_paint_callbacks(callbacks, True) if not self.draw_needs_refresh: - self.present_fbo(0, 0, bw, bh, flush) + self.present_fbo(context, 0, 0, bw, bh, flush) def copy_fbo(self, w : int, h : int, sx : int=0, sy : int=0, dx : int=0, dy : int=0) -> None: log("copy_fbo%s", (w, h, sx, sy, dx, dy)) @@ -742,28 +743,32 @@ def swap_fbos(self) -> None: self.textures[TEX_TMP_FBO] = tmp - def present_fbo(self, x : int, y : int, w : int, h : int, flush=0) -> None: + def present_fbo(self, context, x : int, y : int, w : int, h : int, flush=0) -> None: log("present_fbo: adding %s to pending paint list (size=%i), flush=%s, paint_screen=%s", (x, y, w, h), len(self.pending_fbo_paint), flush, self.paint_screen) + if not context: + raise RuntimeError("missing OpenGL paint context") self.pending_fbo_paint.append((x, y, w, h)) if not self.paint_screen: return #flush>0 means we should wait for the final flush=0 paint if flush==0 or not PAINT_FLUSH: self.record_fps_event() - self.managed_present_fbo() + self.managed_present_fbo(context) - def managed_present_fbo(self) -> None: + def managed_present_fbo(self, context) -> None: + if not context: + raise RuntimeError("missing opengl paint context") try: with paint_context_manager: - self.do_present_fbo() + self.do_present_fbo(context) except Exception as e: log.error("Error presenting FBO:") log.estr(e) log("Error presenting FBO", exc_info=True) self.last_present_fbo_error = str(e) - def do_present_fbo(self) -> None: + def do_present_fbo(self, context) -> None: bw, bh = self.size ww, wh = self.render_size rect_count = len(self.pending_fbo_paint) @@ -783,10 +788,10 @@ def do_present_fbo(self) -> None: glBindFramebuffer(GL_FRAMEBUFFER, 0) glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0) - left, top, right, bottom = self.offsets - #viewport for clearing the whole window: - glViewport(0, 0, left+ww+right, top+wh+bottom) + scale = context.get_scale_factor() + left, top, right, bottom = self.offsets + glViewport(0, 0, int((left+ww+right)*scale), int((top+wh+bottom)*scale)) if self._alpha_enabled: # transparent background: glClearColor(0.0, 0.0, 0.0, 0.0) @@ -796,10 +801,12 @@ def do_present_fbo(self) -> None: if left or top or right or bottom: self.gl_clear_color_buffer() - #from now on, take the offsets into account: - glViewport(left, top, ww, wh) + #from now on, take the offsets and scaling into account: + viewport = int(left*scale), int(top*scale), int(ww*scale), int(wh*scale) + log(f"window viewport for {self.render_size=} and {self.offsets} with scale factor {scale}: {viewport}") + glViewport(*viewport) target = GL_TEXTURE_RECTANGLE_ARB - if ww!=bw or wh!=bh: + if ww!=bw or wh!=bh or scale!=1: glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR) @@ -1006,7 +1013,7 @@ def set_cursor_data(self, cursor_data) -> None: pixels = cursor_data[8] def gl_upload_cursor(context): if context: - self.gl_init() + self.gl_init(context) self.upload_rgba_texture(self.textures[TEX_CURSOR], cw, ch, pixels) self.with_gl_context(gl_upload_cursor) @@ -1119,9 +1126,9 @@ def paint_nvdec(gl_context): self.idle_add(self.do_paint_rgb, rgb_format, img.get_pixels(), x, y, w, h, width, height, img.get_rowstride(), options, callbacks) - def cuda_buffer_to_pbo(self, cuda_buffer, rowstride:int, src_y:int, height:int, stream): + def cuda_buffer_to_pbo(self, gl_context, cuda_buffer, rowstride:int, src_y:int, height:int, stream): #must be called with an active cuda context, and from the UI thread - self.gl_init() + self.gl_init(gl_context) pbo = glGenBuffers(1) size = rowstride*height glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo) @@ -1169,8 +1176,8 @@ def paint_nvdec(self, gl_context, encoding, img_data, x : int, y : int, width : strides = img.get_rowstride() height = img.get_height() try: - y_pbo = self.cuda_buffer_to_pbo(cuda_buffer, strides[0], 0, height, stream) - uv_pbo = self.cuda_buffer_to_pbo(cuda_buffer, strides[1], roundup(height, 2), height//2, stream) + y_pbo = self.cuda_buffer_to_pbo(gl_context, cuda_buffer, strides[0], 0, height, stream) + uv_pbo = self.cuda_buffer_to_pbo(gl_context, cuda_buffer, strides[1], roundup(height, 2), height//2, stream) except LogicError as e: #disable nvdec from now on: self.nvdec_decoder = None @@ -1203,7 +1210,7 @@ def paint_nvjpeg(self, gl_context, encoding, img_data, x : int, y : int, width : raise ValueError(f"unexpected rgb format {rgb_format}") #'pixels' is a cuda buffer: cuda_buffer = img.get_pixels() - pbo = self.cuda_buffer_to_pbo(cuda_buffer, img.get_rowstride(), 0, img.get_height(), stream) + pbo = self.cuda_buffer_to_pbo(gl_context, cuda_buffer, img.get_rowstride(), 0, img.get_height(), stream) cuda_buffer.free() 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 : self.paint_box(encoding, x, y, width, height) # Present update to screen if not self.draw_needs_refresh: - self.present_fbo(x, y, width, height, options.intget("flush", 0)) + self.present_fbo(gl_context, x, y, width, height, options.intget("flush", 0)) # present_fbo has reset state already fire_paint_callbacks(callbacks) glDeleteBuffers(1, [pbo]) @@ -1293,7 +1300,7 @@ def gl_paint_rgb(self, context, rgb_format:str, img_data, try: upload, img_data = self.pixels_for_upload(img_data) - self.gl_init() + self.gl_init(context) scaling = width!=render_width or height!=render_height #convert it to a GL constant: @@ -1339,7 +1346,7 @@ def gl_paint_rgb(self, context, rgb_format:str, img_data, self.paint_box(options.strget("encoding"), x, y, render_width, render_height) # Present update to screen if not self.draw_needs_refresh: - self.present_fbo(x, y, render_width, render_height, options.intget("flush", 0)) + self.present_fbo(context, x, y, render_width, render_height, options.intget("flush", 0)) # present_fbo has reset state already fire_paint_callbacks(callbacks) return @@ -1404,7 +1411,7 @@ def do_gl_paint_planar(self, context, shader, flush:int, encoding:str, img, log("%s._do_paint_rgb(..) no OpenGL context!", self) fire_paint_callbacks(callbacks, False, "failed to get a gl context") return - self.gl_init() + self.gl_init(context) scaling = enc_width!=width or enc_height!=height self.update_planar_textures(enc_width, enc_height, img, pixel_format, scaling=scaling, pbo=options.get("pbo")) @@ -1419,7 +1426,7 @@ def do_gl_paint_planar(self, context, shader, flush:int, encoding:str, img, fire_paint_callbacks(callbacks, True) # Present it on screen if not self.draw_needs_refresh: - self.present_fbo(x, y, width, height, flush) + self.present_fbo(context, x, y, width, height, flush) img.free() return except GLError as e: @@ -1578,6 +1585,6 @@ def gl_expose_rect(self, rect=None) -> None: rect = (0, 0, w, h) def expose(context): if context: - self.gl_init() - self.present_fbo(*rect) + self.gl_init(context) + self.present_fbo(context, *rect) self.with_gl_context(expose) diff --git a/xpra/client/gl/gtk3/client_window.py b/xpra/client/gl/gtk3/client_window.py index 9085209f49..05979671ba 100644 --- a/xpra/client/gl/gtk3/client_window.py +++ b/xpra/client/gl/gtk3/client_window.py @@ -85,9 +85,9 @@ def magic_key(self, *args) -> None: if self.border: self.border.toggle() if b: - with b.gl_context(): - b.gl_init() - b.present_fbo(0, 0, *b.size) + with b.gl_context() as ctx: + b.gl_init(ctx) + b.present_fbo(ctx, 0, 0, *b.size) self.repaint(0, 0, *self._size) log("gl magic_key%s border=%s, backing=%s", args, self.border, b) diff --git a/xpra/client/gl/gtk3/drawing_area.py b/xpra/client/gl/gtk3/drawing_area.py index bc95089695..a1f72a90e4 100644 --- a/xpra/client/gl/gtk3/drawing_area.py +++ b/xpra/client/gl/gtk3/drawing_area.py @@ -85,6 +85,8 @@ def gl_context(self): if not gdk_window: raise RuntimeError(f"backing {b} does not have a gdk window!") self.window_context = self.context.get_paint_context(gdk_window) + if not self.window_context: + raise RuntimeError(f"failed to get an OpenGL window context for {gdk_window} from {self.context}") return self.window_context def do_gl_show(self, rect_count) -> None: @@ -104,6 +106,6 @@ def close_gl_config(self) -> None: def draw_fbo(self, _context) -> None: w, h = self.size - with self.gl_context(): - self.gl_init() - self.present_fbo(0, 0, w, h) + with self.gl_context() as ctx: + self.gl_init(ctx) + self.present_fbo(ctx, 0, 0, w, h) diff --git a/xpra/platform/darwin/gl_context.py b/xpra/platform/darwin/gl_context.py index 89a57f4884..0124d0ac8d 100644 --- a/xpra/platform/darwin/gl_context.py +++ b/xpra/platform/darwin/gl_context.py @@ -68,6 +68,7 @@ class AGLContext: def __init__(self, alpha=True): self.alpha = alpha + self.scale_factor = 1.0 self.gl_context : NSOpenGLContext | None = None self.nsview_ptr : int = 0 self.window_context : AGLWindowContext | None = None @@ -165,11 +166,15 @@ def get_paint_context(self, gdk_window) -> AGLWindowContext: self.gl_context.setValues_forParameter_([0], NSOpenGLCPSurfaceOpacity) enable_transparency(gdk_window) self.window_context = AGLWindowContext(self.gl_context, nsview) + self.scale_factor = nsview.screen().backingScaleFactor() return self.window_context def __del__(self): self.destroy() + def get_scale_factor(self) -> float: + return self.scale_factor + def destroy(self) -> None: c = self.window_context log("AGLContext.destroy() window_context=%s", c) diff --git a/xpra/platform/posix/gl_context.py b/xpra/platform/posix/gl_context.py index c8379be165..4448740c7d 100644 --- a/xpra/platform/posix/gl_context.py +++ b/xpra/platform/posix/gl_context.py @@ -8,7 +8,7 @@ from OpenGL import GLX from OpenGL.GL import GL_VENDOR, GL_RENDERER, glGetString -from xpra.util.env import envbool +from xpra.util.env import envbool, envfloat from xpra.client.gl.check import check_PyOpenGL_support from xpra.x11.bindings.display_source import get_display_ptr #@UnresolvedImport from xpra.gtk.error import xsync @@ -19,6 +19,9 @@ DOUBLE_BUFFERED = envbool("XPRA_OPENGL_DOUBLE_BUFFERED", True) +SCALE_FACTOR = envfloat("XPRA_OPENGL_SCALE_FACTOR", 1) +if SCALE_FACTOR<=0 or SCALE_FACTOR>10: + raise ValueError(f"invalid scale factor {SCALE_FACTOR}") GLX_ATTRIBUTES : dict[Any,str] = { @@ -92,6 +95,9 @@ def swap_buffers(self) -> None: assert self.valid, "GLX window context is no longer valid" GLX.glXSwapBuffers(self.xdisplay, self.xid) + def get_scale_factor(self) -> float: + return SCALE_FACTOR + def __repr__(self): return "GLXWindowContext(%#x)" % self.xid diff --git a/xpra/platform/win32/gl_context.py b/xpra/platform/win32/gl_context.py index fd8a499ef8..d79bc5a00a 100644 --- a/xpra/platform/win32/gl_context.py +++ b/xpra/platform/win32/gl_context.py @@ -41,6 +41,7 @@ def DefWndProc(hwnd, msg, wParam, lParam): return DefWindowProcA(hwnd, msg, wParam, lParam) + class WGLWindowContext: def __init__(self, hwnd:int, hdc:int, context): @@ -77,6 +78,9 @@ def swap_buffers(self): log("swap_buffers: calling SwapBuffers(%#x)", self.paint_hdc) SwapBuffers(self.paint_hdc) + def get_scale_factor(self) -> float: + return 1 + def __repr__(self): return "WGLWindowContext(%#x)" % self.hwnd