Clarisse 5.0 SP8 SDK  5.0.5.8.0
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Friends Groups Pages
Writing a custom Display Driver

Table of Contents

This topic covers how to write a display driver. Please be aware that writing a Display Driver involves a good understanding of the principles of multi-threaded programming.

What is a Display Driver?

If you're looking to redirect Clarisse render output, for example, to display an image in a custom widget, you'll need to create a Display Driver. A Display Driver is an abstract class that lets you grab the output of the rendering engine in real-time. In Clarisse, the Image View defines a Display Driver and connects it to the image chosen by the user. This is how the Image View in Clarisse is able to display the render progress.

There are no limitations to the number of display drivers connected to an image.

Image evaluation

In Clarisse, images are evaluated by background worker threads. Each image has a predefined set of qualities, acting as multipliers to the image resolution and sampling quality when applicable. To render an image, the evaluation must request a specific quality. Qualities are processed one at the time and they can range from 1/16th of the resolution to full resolution. Please note the complete set of qualities is defined in the ModuleImage::Quality enumeration.

The IOHelpersDisplayDriver class

IOHelpersDisplayDriver is an abstract Display Driver that you must inherit from to retrieve real-time information. There are two categories of methods. The ones that allows you to manage connection and disconnection from a ModuleImage and others that actually receive real-time information from the connected image during its rendering.

When display drivers are connected to an image, they are notified, during rendering, in the following order:

  1. IOHelpersDisplayDriver::on_init_render is called when the rendering starts.
  2. IOHelpersDisplayDriver::on_highlight_region gives the regions that are going to be rendered. It's useful if you wish to display which part of the image are currently being rendered.
  3. IOHelpersDisplayDriver::on_draw_region gives all the regions that have been completed between two calls.
  4. IOHelpersDisplayDriver::on_end_render is called when the rendering ends.
  5. IOHelpersDisplayDriver::on_image_level_update is called when the image of a specific quality is completed.

Depending on the speed of the render, IOHelpersDisplayDriver::on_highlight_region (2) and IOHelpersDisplayDriver::on_draw_region (3) may be called multiple times or even skipped. There are certain cases when the rendering is very quick and only IOHelpersDisplayDriver::on_image_level_update (5) is called skipping IOHelpersDisplayDriver::on_init_render (1). IOHelpersDisplayDriver::on_init_render (1) and IOHelpersDisplayDriver::on_end_render (4) go in pair: if the first one is called, the second one will be also called. The same way if the first one is skipped then the second one is skipped too.

Finally another method IOHelpersDisplayDriver::on_progress_update can be called, once in a while, to retrieve the actual rendering percentage of the current image (from 0.0, not started, to 1.0, completed).

A working example in Python

In this following example, we are going to create a floating window, a push button and a framebuffer display. When the user press the button, the display driver connects to the image currently selected in the application. It then request the image to be evaluated from the lower quality to the full resolution.

There are two classes that are important in this example: DisplayDriver and MyFrameBuffer. The DisplayDriver, here, does all the job and build an image made of buckets. Each rendered buckets are pushed in a list. These buckets are then displayed by MyFrameBuffer.

1 #
2 # Copyright (C) 2009 - 2020 Isotropix SAS. All rights reserved.
3 #
4 # The information in this file is provided for the exclusive use of
5 # the software licensees of Isotropix. Contents of this file may not
6 # be distributed, copied or duplicated in any form, in whole or in
7 # part, without the prior written permission of Isotropix SAS.
8 #
9 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
12 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
13 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
14 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
15 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
16 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
17 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
18 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
19 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
20 #
21 
22 import threading
23 
24 class DisplayDriver(ix.api.IOHelpersDisplayDriver):
25  """
26  Custom display driver that connects and listens to a Clarisse Image (ModuleImage).
27  It inherits from IOHelpersDisplayDriver which is an abstract display driver helper class.
28  """
29 
30  def __init__(self, framebuffer):
31  ix.api.IOHelpersDisplayDriver.__init__(self)
32  self.framebuffer = framebuffer
33  self.buckets = ix.api.GMathVec4iVector()
34  self.rendered_regions = ix.api.GMathVec4iVector()
35  self.progress_image_w = 0
36  self.progress_image_h = 0
37 
38  # Building the channel list that we need for the display: r, g, b
39  self.channels = ix.api.CoreStringVector()
40  self.channels.add('r')
41  self.channels.add('g')
42  self.channels.add('b')
43  self.tiles = []
44 
45  def on_init_render(self, init_data):
46  """Called by the rendering thread when it starts to render the image."""
47 
48  self.framebuffer.lock()
49 
50  self.buckets.clear()
51  self.rendered_regions.clear()
52  self.tiles = []
53 
54  self.framebuffer.image = None
55  canvas = init_data.progress_image.get_canvas()
56  self.progress_image_w = canvas.get_width()
57  self.progress_image_h = canvas.get_height()
58 
59  self.framebuffer.unlock()
60 
61  self.framebuffer.must_draw_final_image = False
62  self.framebuffer.draw_final_image(init_data.quality, False)
63  self.framebuffer.redraw()
64 
65  def on_end_render(self):
66  """Called by the rendering thread when the image is completed."""
67 
68  self.framebuffer.lock()
69  self.buckets.clear()
70  self.rendered_regions.clear()
71  self.tiles = []
72  self.framebuffer.unlock()
73 
74  def on_highlight_region(self, regions):
75  """Called by the rendering thread when starting to render new regions in the image."""
76 
77  self.framebuffer.lock()
78  self.buckets = ix.api.GMathVec4iVector()
79  self.buckets.copy_from(regions)
80  self.framebuffer.unlock()
81  self.framebuffer.redraw()
82 
83  def on_image_level_update(self, quality, image):
84  """Called by the rendering thread when the connected image completes a render quality."""
85 
86  self.framebuffer.lock()
87 
88  self.framebuffer.image = None
89  self.framebuffer.must_draw_final_image = False
90  self.progress_image_w = image.get_canvas().get_width()
91  self.progress_image_h = image.get_canvas().get_height()
92  self.framebuffer.draw_final_image(quality, True)
93  self.buckets.clear()
94  self.rendered_regions.clear()
95  self.tiles = []
96 
97  self.framebuffer.unlock()
98  self.framebuffer.redraw()
99 
100  def on_draw_region(self, progress_image, regions):
101  """Called by the rendering thread when new regions are available to display."""
102 
103  self.framebuffer.lock()
104 
105  # Append the new regions/tiles to our list as we don't want to miss any
106  self.rendered_regions.append(regions)
107  canvas = progress_image.get_canvas()
108  bitmap = ix.api.ImageHelperBitmap()
109  for i in range(regions.get_count()):
110  r = regions[i]
111  # Create a 8-bit bitmap tile from the progress image
112  ix.api.ImageHelper.create_bitmap(bitmap, progress_image, r[0], r[1], r[2], r[3], self.channels)
113  # Create a GuiImage but you could create a QImage instead
114  tile = ix.api.GuiImage(bitmap.get_buffer(), bitmap.get_width(), bitmap.get_height(), self.channels.get_count())
115  # Append our new tile to the list to draw it later on
116  self.tiles.append(tile)
117 
118  self.framebuffer.unlock()
119  self.framebuffer.redraw()
120 
121 
122 class MyFrameBuffer(ix.api.GuiWidget):
123  """
124  Widget displaying the image.
125  """
126 
127  def __init__(self, parent, x, y, w, h):
128  ix.api.GuiWidget.__init__(self, parent, x, y, w, h)
129  self.driver = DisplayDriver(self)
130  self.buckets = ix.api.GMathVec4iVector()
131  self.buckets_lock = threading.RLock()
132  self.must_draw_final_image = False
133  self.image = None
134 
135  def lock(self):
136  self.buckets_lock.acquire()
137 
138  def unlock(self):
139  self.buckets_lock.release()
140 
141  def draw(self, dc):
142  if not self.driver.is_connected():
143  return
144 
145  if not self.must_draw_final_image:
146  regions = ix.api.GMathVec4iVector()
147  rendered_regions = ix.api.GMathVec4iVector()
148  dc.draw_rectf(self.get_x(), self.get_y(), self.get_width(), self.get_height(), 0,0,0)
149 
150  self.lock()
151 
152  regions = self.driver.buckets
153  rendered_regions = self.driver.rendered_regions
154 
155  # Draw the buckets that are currently being rendered
156  if regions.get_count() != 0:
157  for i in range(regions.get_count()):
158  r = regions[i]
159  dc.draw_rect(r[0] + self.get_x(), r[1] + self.get_y(), r[2], r[3], 255, 255, 255)
160 
161  # Draw all rendered buckets
162  tiles = self.driver.tiles
163  if rendered_regions.get_count() != 0:
164  i = 0
165  for tile in tiles:
166  r = rendered_regions[i]
167  tile.draw(r[0] + self.get_x(), r[1] + self.get_y())
168  i += 1
169  dc.draw_rect(self.get_x(), self.get_y(), self.driver.progress_image_w, self.driver.progress_image_h, 255,0,0)
170  self.unlock()
171  else:
172  dc.draw_rectf(self.get_x(), self.get_y(), self.get_width(), self.get_height(), 0,0,0)
173  if self.image != None:
174  self.image.draw(self.get_x(), self.get_y())
175  dc.draw_rect(self.get_x(), self.get_y(), self.image.get_width(), self.image.get_height(), 255, 255, 255)
176 
177  def draw_final_image(self, quality, flag):
178  must_redraw = False
179  self.lock()
180 
181  if flag != self.must_draw_final_image:
182  self.must_draw_final_image = flag
183  must_redraw = True
184  if not flag:
185  # Clear the image
186  self.image = None
187  else:
188  bitmap = ix.api.ImageHelperBitmap()
189  module = self.driver.get_image()
190  img = module.get_image(quality)
191  ix.api.ImageHelper.create_bitmap(bitmap, img, 0, 0, img.get_canvas().get_width(), img.get_canvas().get_height(), self.driver.channels)
192  self.image = ix.api.GuiImage(bitmap.get_buffer(), bitmap.get_width(), bitmap.get_height(), self.driver.channels.get_count())
193 
194  self.unlock()
195  if must_redraw:
196  self.redraw()
197 
198 class MyButton(ix.api.GuiPushButton):
199  """
200  Custom button that connects the display driver to the selected image and requests its render and display.
201  """
202 
203  def __init__(self, parent, x, y, w, h):
204  ix.api.GuiPushButton.__init__(self, parent, x, y, w, h, "Connect to Selected Image")
205  self.connect(self, 'EVT_ID_PUSH_BUTTON_CLICK', self.on_click)
206 
207  def on_click(self, sender, evtid):
208  if ix.selection.is_empty():
209  ix.log_info("Select an Image item.")
210  return
211 
212  item = ix.selection[0]
213  if item is not None and item.is_kindof("Image") == False:
214  ix.log_info("The selected item is not an Image")
215  return
216 
217  framebuffer = self.get_parent().framebuffer
218 
219  if framebuffer.driver.is_connected():
220  framebuffer.driver.disconnect()
221 
222  # Clear the FrameBuffer
223  framebuffer.lock()
224  framebuffer.buckets.clear()
225  framebuffer.driver.rendered_regions.clear()
226  framebuffer.tiles = []
227  framebuffer.unlock()
228 
229  # Connect the display driver to the selected image
230  if framebuffer.driver.connect(item.get_module()) == False:
231  ix.log_warning("Failed to connect to Image {}".format(item.get_full_name()))
232  return
233 
234  self.get_parent().set_title("Python Render Window: " + item.get_full_name())
235 
236  # Request a display of the selected image
237  if item.get_module().is_image_dirty(ix.api.ModuleImageQuality.QUALITY_FULL):
238  # The image is dirty (not rendered/finished): start the render
239  framebuffer.draw_final_image(ix.api.ModuleImageQuality.QUALITY_FULL, False)
240  # Launch a progressive evaluation on the image from the lowest quality to full quality.
241  # This call is not blocking and exits immediately. It schedules the evaluation of the image
242  # in Clarisse's evaluation manager.
243  framebuffer.driver.get_image().compute_image(ix.api.ModuleImageQuality.QUALITY_ONE_SIXTEENTH, ix.api.ModuleImageQuality.QUALITY_FULL)
244  else:
245  # The image is clean (finished), display it
246  framebuffer.draw_final_image(ix.api.ModuleImageQuality.QUALITY_FULL, True)
247 
248 class MyRenderWindow(ix.api.GuiWindow):
249  """
250  Main window showing the button and the image.
251  """
252 
253  def __init__(self, application, x, y, w, h):
254  ix.api.GuiWindow.__init__(self, application, x, y, w, h, "Python Render Window: (none)")
255  self.button = MyButton(self, 0, 0, 256, 22)
256  self.framebuffer = MyFrameBuffer(self, 0, 22, w, h - 22)
257  self.framebuffer.set_constraints(ix.api.GuiWidget.CONSTRAINT_LEFT, ix.api.GuiWidget.CONSTRAINT_TOP, ix.api.GuiWidget.CONSTRAINT_RIGHT, ix.api.GuiWidget.CONSTRAINT_BOTTOM)
258 
259  def __del__(self):
260  # Disconnect the display driver when the window is destroyed.
261  self.framebuffer.driver.disconnect()
262 
263  # Force kill the display driver.
264  # Note: there seems there's a destruction bug in Gui when exposed to Python.
265  # This isn't necessary if using PyQt.
266  self.framebuffer.driver = None
267 
268 
269 # Create the render window
270 render_window = MyRenderWindow(ix.application, 0, 0, 640, 480)
271 render_window.show()
272 
273 # Event loop to make the window non-blocking and allow Clarisse to process events
274 while render_window.is_shown():
275  ix.application.check_for_events(True)