Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/gl/gl_window_backing_base.py : 49%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2013 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2012-2020 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7import os
8import time
9from ctypes import c_char_p
11from OpenGL import version as OpenGL_version
12from OpenGL.error import GLError
13from OpenGL.GL import (
14 GL_PROJECTION, GL_MODELVIEW,
15 GL_UNPACK_ROW_LENGTH, GL_UNPACK_ALIGNMENT,
16 GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER, GL_NEAREST,
17 GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT,
18 GL_LUMINANCE, GL_LINEAR,
19 GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2, GL_QUADS, GL_LINE_LOOP, GL_LINES, GL_COLOR_BUFFER_BIT,
20 GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER,
21 GL_DONT_CARE, GL_TRUE, GL_DEPTH_TEST, GL_SCISSOR_TEST, GL_LIGHTING, GL_DITHER,
22 GL_RGB, GL_RGBA, GL_BGR, GL_BGRA, GL_RGBA8, GL_RGB8, GL_RGB10_A2, GL_RGB565, GL_RGB5_A1, GL_RGBA4, GL_RGBA16,
23 GL_UNSIGNED_INT_2_10_10_10_REV, GL_UNSIGNED_INT_10_10_10_2, GL_UNSIGNED_SHORT_5_6_5,
24 GL_BLEND, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA,
25 GL_TEXTURE_MAX_LEVEL, GL_TEXTURE_BASE_LEVEL,
26 GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST,
27 glTexEnvi, GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE,
28 glHint,
29 glBlendFunc,
30 glActiveTexture, glTexSubImage2D,
31 glGetString, glViewport, glMatrixMode, glLoadIdentity, glOrtho,
32 glGenTextures, glDisable,
33 glBindTexture, glPixelStorei, glEnable, glBegin, glFlush,
34 glTexParameteri,
35 glTexImage2D,
36 glMultiTexCoord2i,
37 glTexCoord2i, glVertex2i, glEnd,
38 glClear, glClearColor, glLineWidth, glColor4f,
39 glDrawBuffer, glReadBuffer,
40 )
41from OpenGL.GL.ARB.texture_rectangle import GL_TEXTURE_RECTANGLE_ARB
42from OpenGL.GL.ARB.vertex_program import (
43 glGenProgramsARB, glBindProgramARB, glProgramStringARB,
44 GL_PROGRAM_ERROR_STRING_ARB, GL_PROGRAM_FORMAT_ASCII_ARB,
45 )
46from OpenGL.GL.ARB.fragment_program import GL_FRAGMENT_PROGRAM_ARB
47from OpenGL.GL.ARB.framebuffer_object import (
48 GL_FRAMEBUFFER, GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER,
49 GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, \
50 glGenFramebuffers, glBindFramebuffer, glFramebufferTexture2D, glBlitFramebuffer,
51 )
53from xpra.os_util import (
54 monotonic_time, strtobytes, hexstr,
55 POSIX,
56 DummyContextManager,
57 )
58from xpra.util import envint, envbool, repr_ellipsized, first_time
59from xpra.client.paint_colors import get_paint_box_color
60from xpra.codecs.codec_constants import get_subsampling_divs
61from xpra.client.window_backing_base import (
62 fire_paint_callbacks, WindowBackingBase,
63 WEBP_PILLOW, SCROLL_ENCODING,
64 )
65from xpra.client.gl.gl_check import GL_ALPHA_SUPPORTED, is_pyopengl_memoryview_safe, get_max_texture_size
66from xpra.client.gl.gl_colorspace_conversions import YUV2RGB_shader, YUV2RGB_FULL_shader, RGBP2RGB_shader
67from xpra.client.gl.gl_spinner import draw_spinner
68from xpra.log import Logger
70log = Logger("opengl", "paint")
71fpslog = Logger("opengl", "fps")
73OPENGL_DEBUG = envbool("XPRA_OPENGL_DEBUG", False)
74PAINT_FLUSH = envbool("XPRA_PAINT_FLUSH", True)
75JPEG_YUV = envbool("XPRA_JPEG_YUV", True)
76WEBP_YUV = envbool("XPRA_WEBP_YUV", True)
77FORCE_CLONE = envbool("XPRA_OPENGL_FORCE_CLONE", False)
78DRAW_REFRESH = envbool("XPRA_OPENGL_DRAW_REFRESH", True)
79FBO_RESIZE = envbool("XPRA_OPENGL_FBO_RESIZE", True)
80FBO_RESIZE_DELAY = envint("XPRA_OPENGL_FBO_RESIZE_DELAY", -1)
81CONTEXT_REINIT = envbool("XPRA_OPENGL_CONTEXT_REINIT", False)
83CURSOR_IDLE_TIMEOUT = envint("XPRA_CURSOR_IDLE_TIMEOUT", 6)
85SAVE_BUFFERS = os.environ.get("XPRA_OPENGL_SAVE_BUFFERS")
86if SAVE_BUFFERS not in ("png", "jpeg", None):
87 log.warn("invalid value for XPRA_OPENGL_SAVE_BUFFERS: must be 'png' or 'jpeg'")
88 SAVE_BUFFERS = None
89if SAVE_BUFFERS:
90 from OpenGL.GL import glGetTexImage #pylint: disable=ungrouped-imports
91 from PIL import Image, ImageOps
94PIXEL_FORMAT_TO_CONSTANT = {
95 "r210" : GL_BGRA,
96 "R210" : GL_RGBA,
97 "BGR" : GL_BGR,
98 "RGB" : GL_RGB,
99 "BGRA" : GL_BGRA,
100 "BGRX" : GL_BGRA,
101 "RGBA" : GL_RGBA,
102 "RGBX" : GL_RGBA,
103 "BGR565": GL_RGB,
104 "RGB565": GL_RGB,
105 }
106PIXEL_FORMAT_TO_DATATYPE = {
107 "r210" : GL_UNSIGNED_INT_2_10_10_10_REV,
108 "R210" : GL_UNSIGNED_INT_10_10_10_2,
109 "RGB565": GL_UNSIGNED_SHORT_5_6_5,
110 "BGR565": GL_UNSIGNED_SHORT_5_6_5,
111 "BGR" : GL_UNSIGNED_BYTE,
112 "RGB" : GL_UNSIGNED_BYTE,
113 "BGRA" : GL_UNSIGNED_BYTE,
114 "BGRX" : GL_UNSIGNED_BYTE,
115 "RGBA" : GL_UNSIGNED_BYTE,
116 "RGBX" : GL_UNSIGNED_BYTE,
117 "YUV420P" : GL_UNSIGNED_BYTE,
118 "YUV422P" : GL_UNSIGNED_BYTE,
119 "YUV444P" : GL_UNSIGNED_BYTE,
120 "GBRP" : GL_UNSIGNED_BYTE,
121 "GBRP16" : GL_UNSIGNED_SHORT,
122 "YUV444P10" : GL_UNSIGNED_SHORT,
123 "YUV444P16" : GL_UNSIGNED_SHORT,
124 }
125CONSTANT_TO_PIXEL_FORMAT = {
126 GL_BGR : "BGR",
127 GL_RGB : "RGB",
128 GL_BGRA : "BGRA",
129 GL_RGBA : "RGBA",
130 }
131INTERNAL_FORMAT_TO_STR = {
132 GL_RGB10_A2 : "RGB10_A2",
133 GL_RGBA8 : "RGBA8",
134 GL_RGB8 : "RGB8",
135 GL_RGB565 : "RGB565",
136 GL_RGB5_A1 : "RGB5_A1",
137 GL_RGBA4 : "RGBA4",
138 GL_RGBA16 : "GL_RGBA16",
139 }
140DATATYPE_TO_STR = {
141 GL_UNSIGNED_INT_2_10_10_10_REV : "UNSIGNED_INT_2_10_10_10_REV",
142 GL_UNSIGNED_INT_10_10_10_2 : "UNSIGNED_INT_10_10_10_2",
143 GL_UNSIGNED_BYTE : "UNSIGNED_BYTE",
144 GL_UNSIGNED_SHORT : "UNSIGNED_SHORT",
145 GL_UNSIGNED_SHORT_5_6_5 : "UNSIGNED_SHORT_5_6_5",
146 }
148#debugging variables:
149GL_DEBUG_OUTPUT = None
150GL_DEBUG_OUTPUT_SYNCHRONOUS = None
151gl_debug_callback = None
152glInitStringMarkerGREMEDY = None
153glStringMarkerGREMEDY = None
154glInitFrameTerminatorGREMEDY = None
155glFrameTerminatorGREMEDY = None
156if OPENGL_DEBUG:
157 try:
158 from OpenGL.GL.KHR.debug import (
159 GL_DEBUG_OUTPUT, GL_DEBUG_OUTPUT_SYNCHRONOUS,
160 glDebugMessageControl, glDebugMessageCallback, glInitDebugKHR,
161 )
162 except ImportError:
163 log("Unable to import GL_KHR_debug OpenGL extension. Debug output will be more limited.")
164 try:
165 from OpenGL.GL.GREMEDY.string_marker import glInitStringMarkerGREMEDY, glStringMarkerGREMEDY
166 from OpenGL.GL.GREMEDY.frame_terminator import glInitFrameTerminatorGREMEDY, glFrameTerminatorGREMEDY
167 from OpenGL.GL import GLDEBUGPROC #@UnresolvedImport
168 def py_gl_debug_callback(source, error_type, error_id, severity, length, message, param):
169 log.error("src %x type %x id %x severity %x length %d message %s, param=%s",
170 source, error_type, error_id, severity, length, message, param)
171 gl_debug_callback = GLDEBUGPROC(py_gl_debug_callback)
172 except ImportError:
173 # This is normal- GREMEDY_string_marker is only available with OpenGL debuggers
174 log("Unable to import GREMEDY OpenGL extension. Debug output will be more limited.")
175 log("OpenGL debugging settings:")
176 log(" GL_DEBUG_OUTPUT=%s, GL_DEBUG_OUTPUT_SYNCHRONOUS=%s", GL_DEBUG_OUTPUT, GL_DEBUG_OUTPUT_SYNCHRONOUS)
177 log(" gl_debug_callback=%s", gl_debug_callback)
178 log(" glInitStringMarkerGREMEDY=%s, glStringMarkerGREMEDY=%s",
179 glInitStringMarkerGREMEDY, glStringMarkerGREMEDY)
180 log(" glInitFrameTerminatorGREMEDY=%s, glFrameTerminatorGREMEDY=%s",
181 glInitFrameTerminatorGREMEDY, glFrameTerminatorGREMEDY)
183zerocopy_upload = False
184if envbool("XPRA_ZEROCOPY_OPENGL_UPLOAD", True):
185 try:
186 import OpenGL_accelerate #@UnresolvedImport
187 assert OpenGL_accelerate
188 except ImportError:
189 pass
190 else:
191 zerocopy_upload = is_pyopengl_memoryview_safe(OpenGL_version.__version__, OpenGL_accelerate.__version__)
194if POSIX:
195 from xpra.gtk_common.error import xsync
196 paint_context_manager = xsync
197else:
198 paint_context_manager = DummyContextManager()
201# Texture number assignment
202# The first four are used to update the FBO,
203# the FBO is what is painted on screen.
204TEX_Y = 0
205TEX_U = 1
206TEX_V = 2
207TEX_RGB = 3
208TEX_FBO = 4 #FBO texture (guaranteed up-to-date window contents)
209TEX_TMP_FBO = 5
210TEX_CURSOR = 6
211N_TEXTURES = 7
213# Shader number assignment
214YUV2RGB_SHADER = 0
215RGBP2RGB_SHADER = 1
216YUV2RGB_FULL_SHADER = 2
219"""
220The logic is as follows:
222We create an OpenGL framebuffer object, which will be always up-to-date with the latest windows contents.
223This framebuffer object is updated with YUV painting and RGB painting. It is presented on screen by drawing a
224textured quad when requested, that is: after each YUV or RGB painting operation, and upon receiving an expose event.
225The use of a intermediate framebuffer object is the only way to guarantee that the client keeps
226an always fully up-to-date window image, which is critical because of backbuffer content losses upon buffer swaps
227or offscreen window movement.
228"""
229class GLWindowBackingBase(WindowBackingBase):
231 RGB_MODES = ["YUV420P", "YUV422P", "YUV444P", "GBRP", "BGRA", "BGRX", "RGBA", "RGBX", "RGB", "BGR"]
232 HAS_ALPHA = GL_ALPHA_SUPPORTED
234 def __init__(self, wid : int, window_alpha : bool, pixel_depth : int=0):
235 self.wid = wid
236 self.texture_pixel_format = None
237 #this is the pixel format we are currently updating the fbo with
238 #can be: "YUV420P", "YUV422P", "YUV444P", "GBRP" or None when not initialized yet.
239 self.pixel_format = None
240 self.textures = None # OpenGL texture IDs
241 self.shaders = None
242 self.texture_size = 0, 0
243 self.gl_setup = False
244 self.debug_setup = False
245 self.border = None
246 self.paint_screen = False
247 self.paint_spinner = False
248 self.offscreen_fbo = None
249 self.tmp_fbo = None
250 self.pending_fbo_paint = []
251 self.last_flush = monotonic_time()
252 self.last_present_fbo_error = None
254 super().__init__(wid, window_alpha and self.HAS_ALPHA)
255 self.init_gl_config()
256 self.init_backing()
257 self.bit_depth = self.get_bit_depth(pixel_depth)
258 self.init_formats()
259 self.draw_needs_refresh = DRAW_REFRESH
260 #the correct check would be this:
261 #self.repaint_all = self.is_double_buffered() or bw!=ww or bh!=wh
262 #but we're meant to be using double-buffered everywhere, so don't bother:
263 self.repaint_all = True
264 self._backing.show()
266 def get_info(self):
267 info = super().get_info()
268 info.update({
269 "type" : "OpenGL",
270 "bit-depth" : self.bit_depth,
271 "pixel-format" : self.pixel_format,
272 "texture-pixel-format" : CONSTANT_TO_PIXEL_FORMAT.get(self.texture_pixel_format, str(self.texture_pixel_format)),
273 "internal-format" : INTERNAL_FORMAT_TO_STR.get(self.internal_format, str(self.internal_format)),
274 })
275 return info
278 def init_gl_config(self):
279 raise NotImplementedError()
281 def init_backing(self):
282 raise NotImplementedError()
284 def gl_context(self):
285 raise NotImplementedError()
287 def do_gl_show(self, rect_count):
288 raise NotImplementedError()
290 def is_double_buffered(self):
291 raise NotImplementedError()
294 def get_bit_depth(self, pixel_depth=0):
295 return pixel_depth or 24
297 def init_formats(self):
298 self.RGB_MODES = list(GLWindowBackingBase.RGB_MODES)
299 if self.bit_depth>32:
300 self.internal_format = GL_RGBA16
301 self.RGB_MODES.append("r210")
302 #self.RGB_MODES.append("GBRP16")
303 elif self.bit_depth==30:
304 self.internal_format = GL_RGB10_A2
305 self.RGB_MODES.append("r210")
306 #self.RGB_MODES.append("GBRP16")
307 elif 0<self.bit_depth<=16:
308 if self._alpha_enabled:
309 if envbool("XPRA_GL_RGBA4", True):
310 self.internal_format = GL_RGBA4
311 else:
312 self.internal_format = GL_RGB5_A1
313 #too much of a waste to enable?
314 self.RGB_MODES.append("r210")
315 else:
316 self.internal_format = GL_RGB565
317 self.RGB_MODES.append("BGR565")
318 self.RGB_MODES.append("RGB565")
319 else:
320 if self.bit_depth not in (0, 24, 32) and first_time("bit-depth-%i" % self.bit_depth):
321 log.warn("Warning: invalid bit depth %i, using 24", self.bit_depth)
322 #assume 24:
323 if self._alpha_enabled:
324 self.internal_format = GL_RGBA8
325 else:
326 self.internal_format = GL_RGB8
327 #(pixels are always stored in 32bpp - but this makes it clearer when we do/don't support alpha)
328 if self._alpha_enabled:
329 self.texture_pixel_format = GL_RGBA
330 else:
331 self.texture_pixel_format = GL_RGB
332 log("init_formats() texture pixel format=%s, internal format=%s, rgb modes=%s",
333 CONSTANT_TO_PIXEL_FORMAT.get(self.texture_pixel_format),
334 INTERNAL_FORMAT_TO_STR.get(self.internal_format),
335 self.RGB_MODES)
337 def get_encoding_properties(self):
338 props = WindowBackingBase.get_encoding_properties(self)
339 if SCROLL_ENCODING:
340 props["encoding.scrolling"] = True
341 props["encoding.bit-depth"] = self.bit_depth
342 return props
345 def __repr__(self):
346 return "GLWindowBacking(%s, %s, %s)" % (self.wid, self.size, self.pixel_format)
348 def init(self, ww : int, wh : int, bw : int, bh : int):
349 #re-init gl projection with new dimensions
350 #(see gl_init)
351 self.render_size = ww, wh
352 if self.size!=(bw, bh):
353 self.gl_setup = False
354 oldw, oldh = self.size
355 self.size = bw, bh
356 if CONTEXT_REINIT:
357 self.close_gl_config()
358 self.init_gl_config()
359 return
360 if FBO_RESIZE:
361 self.resize_fbo(oldw, oldh, bw, bh)
363 def resize_fbo(self, oldw : int, oldh : int, bw : int, bh : int):
364 try:
365 context = self.gl_context()
366 except Exception:
367 context = None
368 log("resize_fbo%s context=%s, offscreen_fbo=%s",
369 (oldw, oldh, bw, bh), context, self.offscreen_fbo)
370 if context is None or self.offscreen_fbo is None:
371 return
372 #if we have a valid context and an existing offscreen fbo,
373 #preserve the existing pixels by copying them onto the new tmp fbo (new size)
374 #and then doing the gl_init() call but without initializing the offscreen fbo.
375 sx, sy, dx, dy, w, h = self.gravity_copy_coords(oldw, oldh, bw, bh)
376 with context:
377 context.update_geometry()
378 #invert Y coordinates for OpenGL:
379 sy = (oldh-h)-sy
380 dy = (bh-h)-dy
381 #re-init our OpenGL context with the new size,
382 #but leave offscreen fbo with the old size
383 self.gl_init(True)
384 #copy offscreen to new tmp:
385 self.copy_fbo(w, h, sx, sy, dx, dy)
386 #make tmp the new offscreen:
387 self.swap_fbos()
388 #now we don't need the old tmp fbo contents any more,
389 #and we can re-initialize it with the correct size:
390 mag_filter = self.get_init_magfilter()
391 self.init_fbo(TEX_TMP_FBO, self.tmp_fbo, bw, bh, mag_filter)
392 self._backing.queue_draw_area(0, 0, bw, bh)
393 if FBO_RESIZE_DELAY>=0:
394 def redraw():
395 context = self.gl_context()
396 if not context:
397 return
398 with context:
399 self.pending_fbo_paint = ((0, 0, bw, bh), )
400 self.do_present_fbo()
401 from gi.repository import GLib
402 GLib.timeout_add(FBO_RESIZE_DELAY, redraw)
405 def gl_marker(self, *msg):
406 log(*msg)
407 if not bool(glStringMarkerGREMEDY):
408 return
409 s = str(msg)
410 c_string = c_char_p(s)
411 glStringMarkerGREMEDY(0, c_string)
413 def gl_frame_terminator(self):
414 log("%s.gl_frame_terminator()", self)
415 # Mark the end of the frame
416 # This makes the debug output more readable especially when doing single-buffered rendering
417 if not bool(glFrameTerminatorGREMEDY):
418 return
419 glFrameTerminatorGREMEDY()
421 def gl_init_debug(self):
422 #ensure python knows which scope we're talking about:
423 global glInitStringMarkerGREMEDY, glStringMarkerGREMEDY
424 global glInitFrameTerminatorGREMEDY, glFrameTerminatorGREMEDY
425 # Ask GL to send us all debug messages
426 if GL_DEBUG_OUTPUT and gl_debug_callback and glInitDebugKHR() is True:
427 glEnable(GL_DEBUG_OUTPUT)
428 glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS)
429 glDebugMessageCallback(gl_debug_callback, None)
430 glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, None, GL_TRUE)
431 # Initialize string_marker GL debugging extension if available
432 if glInitStringMarkerGREMEDY and glInitStringMarkerGREMEDY() is True:
433 log.info("Extension GL_GREMEDY_string_marker available. Will output detailed information about each frame.")
434 else:
435 # General case - running without debugger, extension not available
436 glStringMarkerGREMEDY = None
437 #don't bother trying again for another window:
438 glInitStringMarkerGREMEDY = None
439 # Initialize frame_terminator GL debugging extension if available
440 if glInitFrameTerminatorGREMEDY and glInitFrameTerminatorGREMEDY() is True:
441 log.info("Enabling GL frame terminator debugging.")
442 else:
443 glFrameTerminatorGREMEDY = None
444 #don't bother trying again for another window:
445 glInitFrameTerminatorGREMEDY = None
447 def gl_init_textures(self):
448 assert self.offscreen_fbo is None
449 assert self.shaders is None
450 assert glGenFramebuffers, "no framebuffer support"
451 self.textures = glGenTextures(N_TEXTURES)
452 self.offscreen_fbo = glGenFramebuffers(1)
453 self.tmp_fbo = glGenFramebuffers(1)
454 log("%s.gl_init_textures() textures: %s, offscreen fbo: %s, tmp fbo: %s",
455 self, self.textures, self.offscreen_fbo, self.tmp_fbo)
457 def gl_init_shaders(self):
458 assert self.shaders is None
459 # Create and assign fragment programs
460 self.shaders = [ 1, 2, 3 ]
461 glGenProgramsARB(3, self.shaders)
462 for name, progid, progstr in (
463 ("YUV2RGB", YUV2RGB_SHADER, YUV2RGB_shader),
464 ("YUV2RGBFULL", YUV2RGB_FULL_SHADER, YUV2RGB_FULL_shader),
465 ("RGBP2RGB", RGBP2RGB_SHADER, RGBP2RGB_shader),
466 ):
467 glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, self.shaders[progid])
468 glProgramStringARB(GL_FRAGMENT_PROGRAM_ARB, GL_PROGRAM_FORMAT_ASCII_ARB, len(progstr), progstr)
469 err = glGetString(GL_PROGRAM_ERROR_STRING_ARB)
470 if err:
471 log.error("OpenGL shader %s failed:", name)
472 log.error(" %s", err)
473 raise Exception("OpenGL shader %s setup failure: %s" % (name, err))
474 log("%s shader initialized", name)
476 def gl_init(self, skip_fbo=False):
477 #must be called within a context!
478 #performs init if needed
479 if not self.debug_setup:
480 self.debug_setup = True
481 self.gl_init_debug()
483 if self.gl_setup:
484 return
485 mt = get_max_texture_size()
486 w, h = self.size
487 if w>mt or h>mt:
488 raise Exception("invalid texture dimensions %ix%i, maximum is %i" % (w, h, mt))
489 self.gl_marker("Initializing GL context for window size %s, backing size %s, max texture size=%i",
490 self.render_size, self.size, mt)
491 # Initialize viewport and matrices for 2D rendering
492 x, _, _, y = self.offsets
493 glViewport(x, y, w, h)
494 glMatrixMode(GL_PROJECTION)
495 glLoadIdentity()
496 glOrtho(0.0, w, h, 0.0, -1.0, 1.0)
497 glMatrixMode(GL_MODELVIEW)
498 # Mesa docs claim: this hint can improve the speed of texturing
499 #when perspective-correct texture coordinate interpolation isn't needed,
500 #such as when using a glOrtho() projection:
501 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST)
502 # Could be more optimal to use vertex arrays:
503 # glEnableClientState(GL_VERTEX_ARRAY)
504 # glEnableClientState(GL_TEXTURE_COORD_ARRAY)
506 # Clear background to transparent black
507 glClearColor(0.0, 0.0, 0.0, 0.0)
509 # we don't use the depth (2D only):
510 glDisable(GL_DEPTH_TEST)
511 glDisable(GL_SCISSOR_TEST)
512 glDisable(GL_LIGHTING)
513 glDisable(GL_DITHER)
514 # only do alpha blending in present_fbo:
515 glDisable(GL_BLEND)
517 # Default state is good for YUV painting:
518 # - fragment program enabled
519 # - YUV fragment program bound
520 # - render to offscreen FBO
521 if self.textures is None:
522 self.gl_init_textures()
524 mag_filter = self.get_init_magfilter()
525 # Define empty tmp FBO
526 self.init_fbo(TEX_TMP_FBO, self.tmp_fbo, w, h, mag_filter)
527 if not skip_fbo:
528 # Define empty FBO texture and set rendering to FBO
529 self.init_fbo(TEX_FBO, self.offscreen_fbo, w, h, mag_filter)
531 target = GL_TEXTURE_RECTANGLE_ARB
532 glBindTexture(target, 0)
534 # Create and assign fragment programs
535 if not self.shaders:
536 self.gl_init_shaders()
538 # Bind program 0 for YUV painting by default
539 glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, self.shaders[YUV2RGB_SHADER])
540 self.gl_setup = True
541 log("gl_init(%s) done", skip_fbo)
543 def get_init_magfilter(self):
544 rw, rh = self.render_size
545 w, h = self.size
546 if rw/w!=rw//w or rh/h!=rh//h:
547 #non integer scaling, use linear magnification filter:
548 return GL_LINEAR
549 return GL_NEAREST
552 def init_fbo(self, texture_index : int, fbo, w : int, h : int, mag_filter):
553 target = GL_TEXTURE_RECTANGLE_ARB
554 glBindTexture(target, self.textures[texture_index])
555 # nvidia needs this even though we don't use mipmaps (repeated through this file):
556 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, mag_filter)
557 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
558 glTexImage2D(target, 0, self.internal_format, w, h, 0, self.texture_pixel_format, GL_UNSIGNED_BYTE, None)
559 glBindFramebuffer(GL_FRAMEBUFFER, fbo)
560 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, self.textures[texture_index], 0)
561 self.gl_clear_color_buffer()
563 def gl_clear_color_buffer(self):
564 try:
565 glClear(GL_COLOR_BUFFER_BIT)
566 except Exception:
567 log("ignoring glClear(GL_COLOR_BUFFER_BIT) error, buggy driver?", exc_info=True)
570 def close_gl_config(self):
571 pass
573 def close(self):
574 self.close_gl_config()
575 #This seems to cause problems, so we rely
576 #on destroying the context to clear textures and fbos...
577 #if self.offscreen_fbo is not None:
578 # glDeleteFramebuffers(1, [self.offscreen_fbo])
579 # self.offscreen_fbo = None
580 #if self.textures is not None:
581 # glDeleteTextures(self.textures)
582 # self.textures = None
583 b = self._backing
584 if b:
585 self._backing = None
586 b.destroy()
589 def paint_scroll(self, scroll_data, options, callbacks): #pylint: disable=arguments-differ
590 flush = options.intget("flush", 0)
591 self.idle_add(self.do_scroll_paints, scroll_data, flush, callbacks)
593 def do_scroll_paints(self, scrolls, flush=0, callbacks=()):
594 log("do_scroll_paints%s", (scrolls, flush))
595 context = self.gl_context()
596 if not context:
597 log("%s.do_scroll_paints(..) no context!", self)
598 fire_paint_callbacks(callbacks, False, "no opengl context")
599 return
600 def fail(msg):
601 log.error("Error: %s", msg)
602 fire_paint_callbacks(callbacks, False, msg)
603 bw, bh = self.size
604 with context:
605 self.copy_fbo(bw, bh)
607 for x,y,w,h,xdelta,ydelta in scrolls:
608 if abs(xdelta)>=bw:
609 fail("invalid xdelta value: %i, backing width is %i" % (xdelta, bw))
610 continue
611 if abs(ydelta)>=bh:
612 fail("invalid ydelta value: %i, backing height is %i" % (ydelta, bh))
613 continue
614 if ydelta==0 and xdelta==0:
615 fail("scroll has no delta!")
616 continue
617 if w<=0 or h<=0:
618 fail("invalid scroll area size: %ix%i" % (w, h))
619 continue
620 #these should be errors,
621 #but desktop-scaling can cause a mismatch between the backing size
622 #and the real window size server-side.. so we clamp the dimensions instead
623 if x+w>bw:
624 w = bw-x
625 if y+h>bh:
626 h = bh-y
627 if x+w+xdelta>bw:
628 w = bw-x-xdelta
629 if w<=0:
630 continue #nothing left!
631 if y+h+ydelta>bh:
632 h = bh-y-ydelta
633 if h<=0:
634 continue #nothing left!
635 if x+xdelta<0:
636 fail("horizontal scroll by %i:" % xdelta
637 +" rectangle %s overflows the backing buffer size %s" % ((x, y, w, h), self.size))
638 continue
639 if y+ydelta<0:
640 fail("vertical scroll by %i:" % ydelta
641 +" rectangle %s overflows the backing buffer size %s" % ((x, y, w, h), self.size))
642 continue
643 #opengl buffer is upside down, so we must invert Y coordinates: bh-(..)
644 glBlitFramebuffer(x, bh-y, x+w, bh-(y+h),
645 x+xdelta, bh-(y+ydelta), x+w+xdelta, bh-(y+h+ydelta),
646 GL_COLOR_BUFFER_BIT, GL_NEAREST)
647 self.paint_box("scroll", x+xdelta, y+ydelta, x+w+xdelta, y+h+ydelta)
648 glFlush()
650 self.swap_fbos()
652 target = GL_TEXTURE_RECTANGLE_ARB
653 #restore normal paint state:
654 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, self.textures[TEX_FBO], 0)
655 glBindFramebuffer(GL_READ_FRAMEBUFFER, self.offscreen_fbo)
656 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self.offscreen_fbo)
657 glBindFramebuffer(GL_FRAMEBUFFER, self.offscreen_fbo)
659 glBindTexture(target, 0)
660 glDisable(target)
661 fire_paint_callbacks(callbacks, True)
662 if not self.draw_needs_refresh:
663 self.present_fbo(0, 0, bw, bh, flush)
665 def copy_fbo(self, w : int, h : int, sx : int=0, sy : int=0, dx : int=0, dy : int=0):
666 log("copy_fbo%s", (w, h, sx, sy, dx, dy))
667 #copy from offscreen to tmp:
668 glBindFramebuffer(GL_READ_FRAMEBUFFER, self.offscreen_fbo)
669 target = GL_TEXTURE_RECTANGLE_ARB
670 glEnable(target)
671 glBindTexture(target, self.textures[TEX_FBO])
672 glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, self.textures[TEX_FBO], 0)
673 glReadBuffer(GL_COLOR_ATTACHMENT0)
675 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self.tmp_fbo)
676 glBindTexture(target, self.textures[TEX_TMP_FBO])
677 glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, target, self.textures[TEX_TMP_FBO], 0)
678 glDrawBuffer(GL_COLOR_ATTACHMENT1)
680 glBlitFramebuffer(sx, sy, sx+w, sy+h,
681 dx, dy, dx+w, dy+h,
682 GL_COLOR_BUFFER_BIT, GL_NEAREST)
684 def swap_fbos(self):
685 log("swap_fbos()")
686 #swap references to tmp and offscreen so tmp becomes the new offscreen:
687 tmp = self.offscreen_fbo
688 self.offscreen_fbo = self.tmp_fbo
689 self.tmp_fbo = tmp
690 tmp = self.textures[TEX_FBO]
691 self.textures[TEX_FBO] = self.textures[TEX_TMP_FBO]
692 self.textures[TEX_TMP_FBO] = tmp
695 def present_fbo(self, x : int, y : int, w : int, h : int, flush=0):
696 log("present_fbo: adding %s to pending paint list (size=%i), flush=%s, paint_screen=%s",
697 (x, y, w, h), len(self.pending_fbo_paint), flush, self.paint_screen)
698 self.pending_fbo_paint.append((x, y, w, h))
699 if not self.paint_screen:
700 return
701 #flush>0 means we should wait for the final flush=0 paint
702 if flush==0 or not PAINT_FLUSH:
703 try:
704 with paint_context_manager:
705 self.do_present_fbo()
706 except Exception as e:
707 log.error("Error presenting FBO:")
708 log.error(" %s", e)
709 log("Error presenting FBO", exc_info=True)
710 self.last_present_fbo_error = str(e)
712 def do_present_fbo(self):
713 bw, bh = self.size
714 ww, wh = self.render_size
715 rect_count = len(self.pending_fbo_paint)
716 if self.is_double_buffered() or bw!=ww or bh!=wh:
717 #refresh the whole window:
718 rectangles = ((0, 0, bw, bh), )
719 else:
720 #paint just the rectangles we have accumulated:
721 rectangles = self.pending_fbo_paint
722 self.pending_fbo_paint = []
723 log("do_present_fbo: painting %s", rectangles)
725 if SAVE_BUFFERS:
726 self.save_FBO()
728 self.gl_marker("Presenting FBO on screen")
729 # Change state to target screen instead of our FBO
730 glBindFramebuffer(GL_FRAMEBUFFER, 0)
731 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
733 left, top, right, bottom = self.offsets
735 #viewport for clearing the whole window:
736 glViewport(0, 0, left+ww+right, top+wh+bottom)
737 if self._alpha_enabled:
738 # transparent background:
739 glClearColor(0.0, 0.0, 0.0, 0.0)
740 else:
741 # black, no alpha:
742 glClearColor(0.0, 0.0, 0.0, 1.0)
743 if left or top or right or bottom:
744 self.gl_clear_color_buffer()
746 #from now on, take the offsets into account:
747 glViewport(left, top, ww, wh)
748 target = GL_TEXTURE_RECTANGLE_ARB
749 if ww!=bw or wh!=bh:
750 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
751 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
753 # Draw FBO texture on screen
754 glEnable(target) #redundant - done in rgb paint state
755 glBindTexture(target, self.textures[TEX_FBO])
756 if self._alpha_enabled:
757 # support alpha channel if present:
758 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
759 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
760 glBegin(GL_QUADS)
761 for x,y,w,h in rectangles:
762 #note how we invert coordinates..
763 tx1, ty1, tx2, ty2 = x, bh-y, x+w, bh-y-h
764 vx1, vy1, vx2, vy2 = x, y, x+w, y+h
765 glTexCoord2i(tx1, ty1)
766 glVertex2i(vx1, vy1) #top-left of window viewport
767 glTexCoord2i(tx1, ty2)
768 glVertex2i(vx1, vy2) #bottom-left of window viewport
769 glTexCoord2i(tx2, ty2)
770 glVertex2i(vx2, vy2) #bottom-right of window viewport
771 glTexCoord2i(tx2, ty1)
772 glVertex2i(vx2, vy1) #top-right of window viewport
773 glEnd()
774 glBindTexture(target, 0)
775 glDisable(target)
777 if self.pointer_overlay:
778 self.draw_pointer()
780 if self.paint_spinner:
781 #add spinner:
782 self.draw_spinner()
784 if self.border and self.border.shown:
785 self.draw_border()
787 # Show the backbuffer on screen
788 glFlush()
789 self.gl_show(rect_count)
790 self.gl_frame_terminator()
792 #restore pbo viewport
793 glViewport(0, 0, bw, bh)
794 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
795 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
796 log("%s(%s, %s)", glBindFramebuffer, GL_FRAMEBUFFER, self.offscreen_fbo)
797 glBindFramebuffer(GL_FRAMEBUFFER, self.offscreen_fbo)
798 log("%s.do_present_fbo() done", self)
800 def save_FBO(self):
801 target = GL_TEXTURE_RECTANGLE_ARB
802 bw, bh = self.size
803 glEnable(target)
804 glBindFramebuffer(GL_READ_FRAMEBUFFER, self.offscreen_fbo)
805 glBindTexture(target, self.textures[TEX_FBO])
806 glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, self.textures[TEX_FBO], 0)
807 glReadBuffer(GL_COLOR_ATTACHMENT0)
808 glViewport(0, 0, bw, bh)
809 size = bw*bh*4
810 from xpra.buffers.membuf import get_membuf #@UnresolvedImport
811 membuf = get_membuf(size)
812 glGetTexImage(target, 0, GL_BGRA, GL_UNSIGNED_BYTE, membuf.get_mem_ptr())
813 pixels = memoryview(membuf).tobytes()
814 img = Image.frombuffer("RGBA", (bw, bh), pixels, "raw", "BGRA", bw*4)
815 img = ImageOps.flip(img)
816 kwargs = {}
817 if not self._alpha_enabled or SAVE_BUFFERS=="jpeg":
818 img = img.convert("RGB")
819 if SAVE_BUFFERS=="jpeg":
820 kwargs = {
821 "quality" : 0,
822 "optimize" : False,
823 }
824 t = time.time()
825 tstr = time.strftime("%H-%M-%S", time.localtime(t))
826 filename = "./W%i-FBO-%s.%03i.%s" % (self.wid, tstr, (t*1000)%1000, SAVE_BUFFERS)
827 log("do_present_fbo: saving %4ix%-4i pixels, %7i bytes to %s", bw, bh, size, filename)
828 img.save(filename, SAVE_BUFFERS, **kwargs)
829 glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
830 glBindTexture(target, 0)
831 glDisable(target)
833 def draw_pointer(self):
834 px, py, _, _, size, start_time = self.pointer_overlay
835 elapsed = monotonic_time()-start_time
836 log("pointer_overlay=%s, elapsed=%.1f, timeout=%s, cursor-data=%s",
837 self.pointer_overlay, elapsed, CURSOR_IDLE_TIMEOUT, (self.cursor_data or [])[:7])
838 if elapsed>=CURSOR_IDLE_TIMEOUT:
839 #timeout - stop showing it:
840 self.pointer_overlay = None
841 return
842 x = px
843 y = py
844 if not self.cursor_data:
845 #paint a fake one:
846 alpha = max(0, (5.0-elapsed)/5.0)
847 lw = 2
848 glLineWidth(lw)
849 glBegin(GL_LINES)
850 glColor4f(0, 0, 0, alpha)
851 glVertex2i(x-size, y-lw//2)
852 glVertex2i(x+size, y-lw//2)
853 glVertex2i(x, y-size)
854 glVertex2i(x, y+size)
855 glEnd()
856 return
858 cw = self.cursor_data[3]
859 ch = self.cursor_data[4]
860 xhot = self.cursor_data[5]
861 yhot = self.cursor_data[6]
862 x = px-xhot
863 y = py-yhot
864 #paint the texture containing the cursor:
865 glActiveTexture(GL_TEXTURE0)
866 target = GL_TEXTURE_RECTANGLE_ARB
867 glEnable(target)
868 glBindTexture(target, self.textures[TEX_CURSOR])
869 glEnable(GL_BLEND)
870 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
871 #glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
873 glBegin(GL_QUADS)
874 glTexCoord2i(0, 0)
875 glVertex2i(x, y)
876 glTexCoord2i(0, ch)
877 glVertex2i(x, y+ch)
878 glTexCoord2i(cw, ch)
879 glVertex2i(x+cw, y+ch)
880 glTexCoord2i(cw, 0)
881 glVertex2i(x+cw, y)
882 glEnd()
884 glDisable(GL_BLEND)
885 glBindTexture(target, 0)
886 glDisable(target)
888 def draw_spinner(self):
889 bw, bh = self.size
890 draw_spinner(bw, bh)
892 def draw_border(self):
893 bw, bh = self.size
894 #double size since half the line will be off-screen
895 log("draw_border: %s", self.border)
896 glLineWidth(self.border.size*2)
897 glBegin(GL_LINE_LOOP)
898 glColor4f(self.border.red, self.border.green, self.border.blue, self.border.alpha)
899 for px,py in ((0, 0), (bw, 0), (bw, bh), (0, bh)):
900 glVertex2i(px, py)
901 glEnd()
904 def validate_cursor(self):
905 cursor_data = self.cursor_data
906 cw = cursor_data[3]
907 ch = cursor_data[4]
908 pixels = cursor_data[8]
909 blen = cw*ch*4
910 if len(pixels)!=blen:
911 log.error("Error: invalid cursor pixel buffer for %ix%i", cw, ch)
912 log.error(" expected %i bytes but got %i (%s)", blen, len(pixels), type(pixels))
913 log.error(" %s", repr_ellipsized(hexstr(pixels)))
914 return False
915 return True
917 def set_cursor_data(self, cursor_data):
918 if (not cursor_data or len(cursor_data)==1) and self.default_cursor_data:
919 cursor_data = ["raw"] + list(self.default_cursor_data)
920 if not cursor_data:
921 return
922 self.cursor_data = cursor_data
923 if not cursor_data:
924 return
925 cw = cursor_data[3]
926 ch = cursor_data[4]
927 pixels = cursor_data[8]
928 if not self.validate_cursor():
929 return
930 context = self.gl_context()
931 if not context:
932 return
933 with context:
934 self.gl_init()
935 self.upload_cursor_texture(cw, ch, pixels)
937 def upload_cursor_texture(self, width : int, height : int, pixels):
938 upload, pixel_data = self.pixels_for_upload(pixels)
939 rgb_format = "RGBA"
940 glActiveTexture(GL_TEXTURE0)
941 target = GL_TEXTURE_RECTANGLE_ARB
942 glEnable(target)
943 glBindTexture(target, self.textures[TEX_CURSOR])
944 self.set_alignment(width, width*4, rgb_format)
945 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
946 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
947 glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
948 glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
949 glTexImage2D(target, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel_data)
950 log("GL cursor %ix%i uploaded %i bytes of %s pixel data using %s",
951 width, height, len(pixels), rgb_format, upload)
952 glBindTexture(target, 0)
953 glDisable(target)
955 def paint_box(self, encoding : str, x : int, y : int, w : int, h : int):
956 #show region being painted if debug paint box is enabled only:
957 if self.paint_box_line_width<=0:
958 return
959 glLineWidth(self.paint_box_line_width+0.5+int(encoding=="scroll")*2)
960 glBegin(GL_LINE_LOOP)
961 color = get_paint_box_color(encoding)
962 log("Painting colored box around %s screen update using: %s", encoding, color)
963 glColor4f(*color)
964 for px,py in ((x, y), (x+w, y), (x+w, y+h), (x, y+h)):
965 glVertex2i(px, py)
966 glEnd()
969 def pixels_for_upload(self, img_data):
970 #prepare the pixel buffer for upload:
971 if isinstance(img_data, memoryview):
972 if not zerocopy_upload:
973 #not safe, make a copy :(
974 return "copy:memoryview.tobytes", img_data.tobytes()
975 return "zerocopy:memoryview", img_data
976 if isinstance(img_data, bytes) and zerocopy_upload:
977 #we can zerocopy if we wrap it:
978 return "zerocopy:bytes-as-memoryview", memoryview(img_data)
979 if isinstance(img_data, bytes):
980 return "copy:bytes", img_data
981 if hasattr(img_data, "raw"):
982 return "zerocopy:mmap", img_data.raw
983 #everything else.. copy to bytes (aka str):
984 return "copy:bytes(%s)" % type(img_data), strtobytes(img_data)
986 def set_alignment(self, width : int, rowstride : int, pixel_format):
987 bytes_per_pixel = len(pixel_format) #ie: BGRX -> 4, Y -> 1, YY -> 2
988 # Compute alignment and row length
989 row_length = 0
990 alignment = 1
991 for a in (2, 4, 8):
992 # Check if we are a-aligned - ! (var & 0x1) means 2-aligned or better, 0x3 - 4-aligned and so on
993 if (rowstride & a-1) == 0:
994 alignment = a
995 # If number of extra bytes is greater than the alignment value,
996 # then we also have to set row_length
997 # Otherwise it remains at 0 (= width implicitely)
998 if (rowstride - width * bytes_per_pixel) >= alignment:
999 row_length = width + (rowstride - width * bytes_per_pixel) // bytes_per_pixel
1000 glPixelStorei(GL_UNPACK_ROW_LENGTH, row_length)
1001 glPixelStorei(GL_UNPACK_ALIGNMENT, alignment)
1002 self.gl_marker("set_alignment%s GL_UNPACK_ROW_LENGTH=%i, GL_UNPACK_ALIGNMENT=%i",
1003 (width, rowstride, pixel_format), row_length, alignment)
1006 def paint_jpeg(self, img_data, x : int, y : int, width : int, height : int, options, callbacks):
1007 if JPEG_YUV and width>=2 and height>=2:
1008 img = self.jpeg_decoder.decompress_to_yuv(img_data)
1009 flush = options.intget("flush", 0)
1010 w = img.get_width()
1011 h = img.get_height()
1012 self.idle_add(self.gl_paint_planar, YUV2RGB_FULL_SHADER, flush, "jpeg", img,
1013 x, y, w, h, width, height, options, callbacks)
1014 else:
1015 img = self.jpeg_decoder.decompress_to_rgb("BGRX", img_data)
1016 self.idle_add(self.do_paint_rgb, "BGRX", img.get_pixels(), x, y, width, height, img.get_rowstride(), options, callbacks)
1018 def paint_webp(self, img_data, x : int, y : int, width : int, height : int, options, callbacks):
1019 subsampling = options.strget("subsampling")
1020 has_alpha = options.boolget("has_alpha")
1021 if subsampling=="YUV420P" and WEBP_YUV and self.webp_decoder and not WEBP_PILLOW and not has_alpha and width>=2 and height>=2:
1022 img = self.webp_decoder.decompress_yuv(img_data)
1023 flush = options.intget("flush", 0)
1024 w = img.get_width()
1025 h = img.get_height()
1026 self.idle_add(self.gl_paint_planar, YUV2RGB_SHADER, flush, "webp", img,
1027 x, y, w, h, width, height, options, callbacks)
1028 return
1029 super().paint_webp(img_data, x, y, width, height, options, callbacks)
1031 def do_paint_rgb(self, rgb_format, img_data,
1032 x : int, y : int, width : int, height : int, render_width : int, render_height : int,
1033 rowstride, options, callbacks):
1034 log("%s.do_paint_rgb(%s, %s bytes, x=%d, y=%d, width=%d, height=%d, rowstride=%d, options=%s)",
1035 self, rgb_format, len(img_data), x, y, width, height, rowstride, options)
1036 x, y = self.gravity_adjust(x, y, options)
1037 context = self.gl_context()
1038 if not context:
1039 log("%s._do_paint_rgb(..) no context!", self)
1040 fire_paint_callbacks(callbacks, False, "no opengl context")
1041 return
1042 if not options.boolget("paint", True):
1043 fire_paint_callbacks(callbacks)
1044 return
1045 try:
1046 rgb_format = rgb_format.decode("utf-8")
1047 except (AttributeError, UnicodeDecodeError):
1048 pass
1049 try:
1050 upload, img_data = self.pixels_for_upload(img_data)
1052 with context:
1053 self.gl_init()
1054 scaling = width!=render_width or height!=render_height
1056 #convert it to a GL constant:
1057 pformat = PIXEL_FORMAT_TO_CONSTANT.get(rgb_format)
1058 assert pformat is not None, "could not find pixel format for %s" % rgb_format
1059 ptype = PIXEL_FORMAT_TO_DATATYPE.get(rgb_format)
1060 assert pformat is not None, "could not find pixel type for %s" % rgb_format
1062 self.gl_marker("%s update at (%d,%d) size %dx%d (%s bytes) to %dx%d, using GL %s format=%s / %s to internal format=%s",
1063 rgb_format, x, y, width, height, len(img_data), render_width, render_height,
1064 upload, CONSTANT_TO_PIXEL_FORMAT.get(pformat), DATATYPE_TO_STR.get(ptype), INTERNAL_FORMAT_TO_STR.get(self.internal_format))
1066 # Upload data as temporary RGB texture
1067 target = GL_TEXTURE_RECTANGLE_ARB
1068 glEnable(target)
1069 glBindTexture(target, self.textures[TEX_RGB])
1070 self.set_alignment(width, rowstride, rgb_format)
1071 mag_filter = GL_NEAREST
1072 if scaling:
1073 mag_filter = GL_LINEAR
1074 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, mag_filter)
1075 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
1076 glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
1077 glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
1078 glTexImage2D(target, 0, self.internal_format, width, height, 0, pformat, ptype, img_data)
1080 # Draw textured RGB quad at the right coordinates
1081 glBegin(GL_QUADS)
1082 glTexCoord2i(0, 0)
1083 glVertex2i(x, y)
1084 glTexCoord2i(0, height)
1085 glVertex2i(x, y+render_height)
1086 glTexCoord2i(width, height)
1087 glVertex2i(x+render_width, y+render_height)
1088 glTexCoord2i(width, 0)
1089 glVertex2i(x+render_width, y)
1090 glEnd()
1092 glBindTexture(target, 0)
1093 glDisable(target)
1094 self.paint_box(options.strget("encoding"), x, y, render_width, render_height)
1095 # Present update to screen
1096 if not self.draw_needs_refresh:
1097 self.present_fbo(x, y, render_width, render_height, options.intget("flush", 0))
1098 # present_fbo has reset state already
1099 fire_paint_callbacks(callbacks)
1100 return
1101 except GLError as e:
1102 message = "OpenGL %s paint failed: %r" % (rgb_format, e)
1103 log("Error in %s paint of %i bytes, options=%s", rgb_format, len(img_data), options, exc_info=True)
1104 except Exception as e:
1105 message = "OpenGL %s paint error: %s" % (rgb_format, e)
1106 log("Error in %s paint of %i bytes, options=%s", rgb_format, len(img_data), options, exc_info=True)
1107 fire_paint_callbacks(callbacks, False, message)
1110 def do_video_paint(self, img, x : int, y : int, enc_width : int, enc_height : int, width : int, height : int, options, callbacks):
1111 if not zerocopy_upload or FORCE_CLONE:
1112 #copy so the data will be usable (usually a str)
1113 img.clone_pixel_data()
1114 pixel_format = img.get_pixel_format()
1115 if pixel_format in ("GBRP10", "YUV444P10"):
1116 #call superclass to handle csc
1117 #which will end up calling paint rgb with r210 data
1118 return super().do_video_paint(img, x, y, enc_width, enc_height, width, height, options, callbacks)
1119 if pixel_format.startswith("GBRP"):
1120 shader = RGBP2RGB_SHADER
1121 else:
1122 shader = YUV2RGB_SHADER
1123 self.idle_add(self.gl_paint_planar, shader, options.intget("flush", 0), options.strget("encoding"), img,
1124 x, y, enc_width, enc_height, width, height, options, callbacks)
1126 def gl_paint_planar(self, shader, flush, encoding, img,
1127 x : int, y : int, enc_width : int, enc_height : int, width : int, height : int,
1128 options, callbacks):
1129 #this function runs in the UI thread, no video_decoder lock held
1130 log("gl_paint_planar%s", (flush, encoding, img, x, y, enc_width, enc_height, width, height, options, callbacks))
1131 x, y = self.gravity_adjust(x, y, options)
1132 try:
1133 pixel_format = img.get_pixel_format()
1134 assert pixel_format in ("YUV420P", "YUV422P", "YUV444P", "GBRP", "GBRP16", "YUV444P16"), \
1135 "sorry the GL backing does not handle pixel format '%s' yet!" % (pixel_format)
1137 context = self.gl_context()
1138 if not context:
1139 log("%s._do_paint_rgb(..) no context!", self)
1140 fire_paint_callbacks(callbacks, False, "failed to get a gl context")
1141 return
1142 with context:
1143 self.gl_init()
1144 scaling = enc_width!=width or enc_height!=height
1145 self.update_planar_textures(enc_width, enc_height, img, pixel_format, scaling=scaling)
1147 # Update FBO texture
1148 x_scale, y_scale = 1, 1
1149 if scaling:
1150 x_scale = width/enc_width
1151 y_scale = height/enc_height
1153 self.render_planar_update(x, y, enc_width, enc_height, x_scale, y_scale, shader)
1154 self.paint_box(encoding, x, y, width, height)
1155 fire_paint_callbacks(callbacks, True)
1156 # Present it on screen
1157 if not self.draw_needs_refresh:
1158 self.present_fbo(x, y, width, height, flush)
1159 img.free()
1160 return
1161 except GLError as e:
1162 message = "OpenGL %s paint failed: %r" % (encoding, e)
1163 log.error("Error painting planar update", exc_info=True)
1164 except Exception as e:
1165 message = "OpenGL %s paint failed: %s" % (encoding, e)
1166 log.error("Error painting planar update", exc_info=True)
1167 log.error(" flush=%i, image=%s, coords=%s, size=%ix%i",
1168 flush, img, (x, y, enc_width, enc_height), width, height)
1169 fire_paint_callbacks(callbacks, False, message)
1171 def update_planar_textures(self, width : int, height : int, img, pixel_format, scaling=False):
1172 assert self.textures is not None, "no OpenGL textures!"
1173 log("%s.update_planar_textures%s", self, (width, height, img, pixel_format))
1175 upload_format = PIXEL_FORMAT_TO_DATATYPE[pixel_format]
1176 divs = get_subsampling_divs(pixel_format)
1177 if self.pixel_format is None or self.pixel_format!=pixel_format or self.texture_size!=(width, height):
1178 self.gl_marker("Creating new planar textures, pixel format %s (was %s), texture size %s (was %s)",
1179 pixel_format, self.pixel_format, (width, height), self.texture_size)
1180 self.pixel_format = pixel_format
1181 self.texture_size = (width, height)
1182 # Create textures of the same size as the window's
1183 for texture, index in ((GL_TEXTURE0, TEX_Y), (GL_TEXTURE1, TEX_U), (GL_TEXTURE2, TEX_V)):
1184 (div_w, div_h) = divs[index]
1185 glActiveTexture(texture)
1186 target = GL_TEXTURE_RECTANGLE_ARB
1187 glBindTexture(target, self.textures[index])
1188 mag_filter = GL_NEAREST
1189 if scaling or (div_w > 1 or div_h > 1):
1190 mag_filter = GL_LINEAR
1191 glTexParameteri(target, GL_TEXTURE_MAG_FILTER, mag_filter)
1192 glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
1193 glTexImage2D(target, 0, GL_LUMINANCE, width//div_w, height//div_h, 0, GL_LUMINANCE, upload_format, None)
1194 #glBindTexture(target, 0) #redundant: we rebind below:
1196 self.gl_marker("updating planar textures: %sx%s %s", width, height, pixel_format)
1197 rowstrides = img.get_rowstride()
1198 img_data = img.get_pixels()
1199 BPP = 2 if pixel_format.endswith("P16") else 1
1200 assert len(rowstrides)==3 and len(img_data)==3
1201 for texture, index, tex_name in (
1202 (GL_TEXTURE0, TEX_Y, pixel_format[0:1]*BPP),
1203 (GL_TEXTURE1, TEX_U, pixel_format[1:2]*BPP),
1204 (GL_TEXTURE2, TEX_V, pixel_format[2:3]*BPP),
1205 ):
1206 div_w, div_h = divs[index]
1207 w = width//div_w
1208 h = height//div_h
1209 if w==0 or h==0:
1210 log.error("Error: zero dimension %ix%i for %s planar texture %s", w, h, pixel_format, tex_name)
1211 log.error(" screen update %s dropped", (width, height))
1212 continue
1213 glActiveTexture(texture)
1215 target = GL_TEXTURE_RECTANGLE_ARB
1216 glBindTexture(target, self.textures[index])
1217 self.set_alignment(w, rowstrides[index], tex_name)
1218 upload, pixel_data = self.pixels_for_upload(img_data[index])
1219 log("texture %s: div=%s, rowstride=%s, %sx%s, data=%s bytes, upload=%s",
1220 index, divs[index], rowstrides[index], w, h, len(pixel_data), upload)
1221 glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, 0)
1222 try:
1223 glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 0)
1224 except Exception:
1225 pass
1226 glTexSubImage2D(target, 0, 0, 0, w, h, GL_LUMINANCE, upload_format, pixel_data)
1227 glBindTexture(target, 0)
1228 #glActiveTexture(GL_TEXTURE0) #redundant, we always call render_planar_update afterwards
1230 def render_planar_update(self, rx : int, ry : int, rw : int, rh : int, x_scale=1, y_scale=1, shader=YUV2RGB_SHADER):
1231 log("%s.render_planar_update%s pixel_format=%s",
1232 self, (rx, ry, rw, rh, x_scale, y_scale, shader), self.pixel_format)
1233 if self.pixel_format not in ("YUV420P", "YUV422P", "YUV444P", "GBRP", "GBRP16", "YUV444P16"):
1234 #not ready to render yet
1235 return
1236 self.gl_marker("painting planar update, format %s", self.pixel_format)
1237 glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, self.shaders[shader])
1238 glEnable(GL_FRAGMENT_PROGRAM_ARB)
1239 for texture, index in ((GL_TEXTURE0, TEX_Y), (GL_TEXTURE1, TEX_U), (GL_TEXTURE2, TEX_V)):
1240 glActiveTexture(texture)
1241 glBindTexture(GL_TEXTURE_RECTANGLE_ARB, self.textures[index])
1243 tw, th = self.texture_size
1244 divs = get_subsampling_divs(self.pixel_format)
1245 log("%s.render_planar_update(..) texture_size=%s, size=%s", self, self.texture_size, self.size)
1246 glBegin(GL_QUADS)
1247 for x,y in ((0, 0), (0, rh), (rw, rh), (rw, 0)):
1248 ax = min(tw, x)
1249 ay = min(th, y)
1250 for texture, index in ((GL_TEXTURE0, TEX_Y), (GL_TEXTURE1, TEX_U), (GL_TEXTURE2, TEX_V)):
1251 (div_w, div_h) = divs[index]
1252 glMultiTexCoord2i(texture, ax//div_w, ay//div_h)
1253 glVertex2i(int(rx+ax*x_scale), int(ry+ay*y_scale))
1254 glEnd()
1255 for texture in (GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2):
1256 glActiveTexture(texture)
1257 glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0)
1258 glDisable(GL_FRAGMENT_PROGRAM_ARB)
1259 glActiveTexture(GL_TEXTURE0)
1262 def gl_show(self, rect_count):
1263 start = monotonic_time()
1264 self.do_gl_show(rect_count)
1265 end = monotonic_time()
1266 flush_elapsed = end-self.last_flush
1267 self.last_flush = end
1268 fpslog("gl_show after %3ims took %2ims, %2i updates", flush_elapsed*1000, (end-start)*1000, rect_count)
1271 def gl_expose_rect(self, rect=None):
1272 if not self.paint_screen:
1273 return
1274 context = self.gl_context()
1275 if not context:
1276 return
1277 if not rect:
1278 w, h = self.size
1279 rect = (0, 0, w, h)
1280 with context:
1281 self.gl_init()
1282 self.present_fbo(*rect)