Skip to content
Snippets Groups Projects
Commit 9e57cdcb authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Aargh. Everything is broken. Trying to use 3D Texture for storing image data.

parent 6e120eee
No related branches found
No related tags found
No related merge requests found
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import sys import sys
import logging import logging
import traceback
import collections import collections
import os.path as op import os.path as op
...@@ -392,3 +393,4 @@ class ImageList(object): ...@@ -392,3 +393,4 @@ class ImageList(object):
listener(self) listener(self)
except Exception as e: except Exception as e:
log.debug('Listener raised exception: {}'.format(e.message)) log.debug('Listener raised exception: {}'.format(e.message))
traceback.print_exc()
...@@ -20,6 +20,7 @@ import OpenGL.arrays.vbo as vbo ...@@ -20,6 +20,7 @@ import OpenGL.arrays.vbo as vbo
# for functionality which is standard in 3.2. # for functionality which is standard in 3.2.
import OpenGL.GL.ARB.instanced_arrays as arbia import OpenGL.GL.ARB.instanced_arrays as arbia
import OpenGL.GL.ARB.draw_instanced as arbdi import OpenGL.GL.ARB.draw_instanced as arbdi
import OpenGL.GL.ARB.texture_rg as arbrg
import fsl.data.fslimage as fslimage import fsl.data.fslimage as fslimage
...@@ -57,11 +58,6 @@ class GLImageData(object): ...@@ -57,11 +58,6 @@ class GLImageData(object):
self.image = image self.image = image
self.canvas = canvas self.canvas = canvas
self.imageBuffer = None
self.colourBuffer = None
self.positionBuffer = None
self.geomBuffer = None
# Here, x,y, and z refer to screen # Here, x,y, and z refer to screen
# coordinates, not image coordinates: # coordinates, not image coordinates:
...@@ -79,13 +75,6 @@ class GLImageData(object): ...@@ -79,13 +75,6 @@ class GLImageData(object):
self.ylen = image.pixdim[canvas.yax] self.ylen = image.pixdim[canvas.yax]
self.zlen = image.pixdim[canvas.zax] self.zlen = image.pixdim[canvas.zax]
dsize = image.data.dtype.itemsize
# byte offset along each axis
self.xstride = image.data.strides[canvas.xax] / dsize
self.ystride = image.data.strides[canvas.yax] / dsize
self.zstride = image.data.strides[canvas.zax] / dsize
# Maximum number of colours used to draw image data # Maximum number of colours used to draw image data
self.colourResolution = 256 self.colourResolution = 256
...@@ -131,50 +120,51 @@ class GLImageData(object): ...@@ -131,50 +120,51 @@ class GLImageData(object):
# float32 # float32
positionData = image.voxToWorld(positionData, axes=(xax, yax)) positionData = image.voxToWorld(positionData, axes=(xax, yax))
positionData = np.array(positionData, dtype=np.float32) positionData = np.array(positionData, dtype=np.float32)
positionData = positionData.ravel('C')
# Define GL buffers for the geometry and position # The image buffers, containing the image data
# data containing the data we just created above imageBuffers = self.initImageBuffer()
geomData = geomData .ravel(order='C') screenPosBuffer = vbo.VBO(positionData, gl.GL_STATIC_DRAW)
positionData = positionData.ravel(order='C') geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW)
geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW)
positionBuffer = vbo.VBO(positionData, gl.GL_STATIC_DRAW)
# The image buffer, containing the image data itself
imageBuffer = self.initImageBuffer()
# The colour buffer, containing a map of
# colours (stored on the GPU as a 1D texture)
colourBuffer = gl.glGenTextures(1)
self.geomBuffer = geomBuffer self.dataBuffer = imageBuffers['dataBuffer']
self.positionBuffer = positionBuffer self.voxXBuffer = imageBuffers['xBuffer']
self.imageBuffer = imageBuffer self.voxYBuffer = imageBuffers['yBuffer']
self.colourBuffer = colourBuffer self.voxZBuffer = imageBuffers['zBuffer']
self.screenPosBuffer = screenPosBuffer
self.geomBuffer = geomBuffer
# Add listeners to this image so the view can be # Add listeners to this image so the view can be
# updated when its display properties are changed # updated when its display properties are changed
self.configDisplayListeners() self.configDisplayListeners()
# Create the colour buffer for the given image
self.updateColourBuffer()
def initImageBuffer(self): def initImageBuffer(self):
""" """
Initialises the OpenGL buffer used to store the data for the given Initialises the OpenGL buffers used to store the data for the given
image. The buffer is stored as an attribute of the image and, if it image. The buffers are stored as an attribute of the image and, if it
has already been created (e.g. by another SliceCanvas object), the has already been created (e.g. by another SliceCanvas object), the
existing buffer is returned. existing buffer is returned. The value stored on the image, and
the value returned by this method, is a dictionary with the following
keys:
- dataBuffer:
- xBuffer:
- yBuffer:
- zBuffer:
""" """
image = self.image image = self.image
try: imageBuffer = image.getAttribute('glBuffer') try: imageBuffer = image.getAttribute('glBuffers')
except: imageBuffer = None except: imageBuffer = None
if imageBuffer is not None: if imageBuffer is not None:
return imageBuffer return imageBuffer
xlen = image.shape[0]
ylen = image.shape[1]
zlen = image.shape[2]
# The image data is normalised to lie # The image data is normalised to lie
# between 0 and 255, and cast to uint8 # between 0 and 255, and cast to uint8
imageData = np.array(image.data, dtype=np.float32) imageData = np.array(image.data, dtype=np.float32)
...@@ -186,12 +176,61 @@ class GLImageData(object): ...@@ -186,12 +176,61 @@ class GLImageData(object):
# so the data, as stored on the GPU, has its first # so the data, as stored on the GPU, has its first
# dimension as the fastest changing. # dimension as the fastest changing.
imageData = imageData.ravel(order='F') imageData = imageData.ravel(order='F')
imageBuffer = vbo.VBO(imageData, gl.GL_STATIC_DRAW)
# Image data is stored on the GPU as a 3D texture
dataBuffer = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_3D, dataBuffer)
gl.glTexParameteri(
gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
gl.glTexParameteri(
gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
gl.glTexParameteri(
gl.GL_TEXTURE_3D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexImage3D(gl.GL_TEXTURE_3D,
0,
arbrg.GL_R8,
xlen,
ylen,
zlen,
0,
gl.GL_RED,
gl.GL_UNSIGNED_BYTE,
imageData)
# x/y/z coordinates are stored as
# VBO arrays in the range [0, 1]
xstep = 1.0 / xlen
ystep = 1.0 / ylen
zstep = 1.0 / zlen
xData = np.arange(0.0+xstep/2, 1.0+xstep/2, 1.0 / xlen, dtype=np.float32)
yData = np.arange(0.0+ystep/2, 1.0+ystep/2, 1.0 / ylen, dtype=np.float32)
zData = np.arange(0.0+zstep/2, 1.0+zstep/2, 1.0 / zlen, dtype=np.float32)
xBuffer = vbo.VBO(xData, gl.GL_STATIC_DRAW)
yBuffer = vbo.VBO(yData, gl.GL_STATIC_DRAW)
zBuffer = vbo.VBO(zData, gl.GL_STATIC_DRAW)
print "Data buffer"
print imageData
print "X buffer"
print xData
print "Y buffer"
print yData
print "Z buffer"
print zData
imageBuffer = {}
imageBuffer['dataBuffer'] = dataBuffer
imageBuffer['xBuffer'] = xBuffer
imageBuffer['yBuffer'] = yBuffer
imageBuffer['zBuffer'] = zBuffer
# And added as an attribute of the image, so # And added as an attribute of the image, so
# other things which want to render the image # other things which want to render the image
# don't need to create another buffer. # don't need to recreate all of those buffers.
image.setAttribute('glBuffer', imageBuffer) image.setAttribute('glBuffers', imageBuffer)
return imageBuffer return imageBuffer
...@@ -232,63 +271,6 @@ class GLImageData(object): ...@@ -232,63 +271,6 @@ class GLImageData(object):
display.addListener(prop, lnrName.format(prop), colourUpdateNeeded) display.addListener(prop, lnrName.format(prop), colourUpdateNeeded)
def updateColourBuffer(self):
"""
Regenerates the colour buffer used to colour image voxels.
"""
display = self.image.display
colourBuffer = self.colourBuffer
# Here we are creating a range of values to be passed
# to the matplotlib.colors.Colormap instance of the
# image display. We scale this range such that data
# values which lie outside the configured display range
# will map to values below 0.0 or above 1.0. It is
# assumed that the Colormap instance is configured to
# generate appropriate colours for these out-of-range
# values.
normalRange = np.linspace(0.0, 1.0, self.colourResolution)
normalStep = 1.0 / (self.colourResolution - 1)
normMin = (display.displayMin - display.dataMin) / \
(display.dataMax - display.dataMin)
normMax = (display.displayMax - display.dataMin) / \
(display.dataMax - display.dataMin)
newStep = normalStep / (normMax - normMin)
newRange = (normalRange - normMin) * (newStep / normalStep)
# Create [self.colourResolution] rgb values,
# spanning the entire range of the image
# colour map
colourmap = display.cmap(newRange)
# The colour data is stored on
# the GPU as 8 bit rgb triplets
colourmap = np.floor(colourmap * 255)
colourmap = np.array(colourmap, dtype=np.uint8)
colourmap = colourmap.ravel(order='C')
# GL texture creation stuff
gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer)
gl.glTexParameteri(
gl.GL_TEXTURE_1D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
gl.glTexParameteri(
gl.GL_TEXTURE_1D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
gl.glTexParameteri(
gl.GL_TEXTURE_1D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
gl.glTexImage1D(gl.GL_TEXTURE_1D,
0,
gl.GL_RGBA8,
self.colourResolution,
0,
gl.GL_RGBA,
gl.GL_UNSIGNED_BYTE,
colourmap)
# The vertex shader positions and colours a single vertex. # The vertex shader positions and colours a single vertex.
vertex_shader = """ vertex_shader = """
...@@ -297,14 +279,19 @@ vertex_shader = """ ...@@ -297,14 +279,19 @@ vertex_shader = """
/* Opacity - constant for a whole image */ /* Opacity - constant for a whole image */
uniform float alpha; uniform float alpha;
/* image data texture */
uniform sampler3D dataBuffer;
/* Current vertex */ /* Current vertex */
attribute vec2 inVertex; attribute vec2 inVertex;
/* Position of the current voxel */ /* Current screen coordinates */
attribute vec2 inPos; attribute vec2 screenPos;
/* Value of the current voxel (in range [0,1]) */ /* voxel coordinates */
attribute float voxValue; attribute float voxX;
attribute float voxY;
attribute float voxZ;
/* Voxel value passed through to fragment shader */ /* Voxel value passed through to fragment shader */
varying float fragVoxValue; varying float fragVoxValue;
...@@ -316,33 +303,28 @@ void main(void) { ...@@ -316,33 +303,28 @@ void main(void) {
* (and perform standard transformation from data * (and perform standard transformation from data
* coordinates to screen coordinates). * coordinates to screen coordinates).
*/ */
gl_Position = gl_ModelViewProjectionMatrix * \ gl_Position = gl_ModelViewProjectionMatrix * \
vec4(inVertex+inPos, 0.0, 1.0); vec4(inVertex + screenPos, 0.0, 1.0);
/* Pass the voxel value through to the shader. */ /* Pass the voxel value through to the shader. */
fragVoxValue = voxValue; vec4 vt = texture3D(dataBuffer, vec3(voxX, voxY, voxZ));
fragVoxValue = vt.r;
} }
""" """
# Fragment shader. Given the current voxel value, looks # Buffer shader. Given the current voxel value, looks
# up the appropriate colour in the colour buffer. # up the appropriate colour in the colour buffer.
fragment_shader = """ fragment_shader = """
#version 120 #version 120
uniform float alpha; uniform float alpha;
uniform sampler1D colourMap; /* RGB colour map, stored as a 1D texture */
varying float fragVoxValue; varying float fragVoxValue;
void main(void) { void main(void) {
vec4 voxTexture = texture1D(colourMap, fragVoxValue); vec3 voxColour = vec3(fragVoxValue, fragVoxValue, fragVoxValue);
vec3 voxColour = voxTexture.rgb; float voxAlpha = alpha;
float voxAlpha = voxTexture.a;
if (voxAlpha > alpha) {
voxAlpha = alpha;
}
gl_FragColor = vec4(voxColour, voxAlpha); gl_FragColor = vec4(voxColour, voxAlpha);
} }
...@@ -551,16 +533,6 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -551,16 +533,6 @@ class SliceCanvas(wxgl.GLCanvas):
of this SliceCanvas object. This method is only called of this SliceCanvas object. This method is only called
once, on the first draw. once, on the first draw.
""" """
# A bit hacky. We can only set the GL context (and create
# the GL data) once something is actually displayed on the
# screen. The _initGLData method is called (asynchronously)
# by the draw() method if it sees that the glReady flag has
# not yet been set. But draw() may be called mored than once
# before _initGLData is called. Here, to prevent
# _initGLData from running more than once, the first time
# it is called it simply overwrites itself with a dummy method.
self._initGLData = lambda s: s
self.context.SetCurrent(self) self.context.SetCurrent(self)
...@@ -568,17 +540,33 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -568,17 +540,33 @@ class SliceCanvas(wxgl.GLCanvas):
shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER), shaders.compileShader(vertex_shader, gl.GL_VERTEX_SHADER),
shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER)) shaders.compileShader(fragment_shader, gl.GL_FRAGMENT_SHADER))
# Indices of all vertex/fragment shader parameters # Indices of all vertex/fragment shader parameters
self.inVertexPos = gl.glGetAttribLocation( self.shaders, 'inVertex') self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha')
self.voxelValuePos = gl.glGetAttribLocation( self.shaders, 'voxValue') self.dataBufferPos = gl.glGetUniformLocation(self.shaders,
self.inPositionPos = gl.glGetAttribLocation( self.shaders, 'inPos') 'dataBuffer')
self.alphaPos = gl.glGetUniformLocation(self.shaders, 'alpha') self.inVertexPos = gl.glGetAttribLocation( self.shaders,
self.colourMapPos = gl.glGetUniformLocation(self.shaders, 'colourMap') 'inVertex')
self.screenPosPos = gl.glGetAttribLocation( self.shaders,
'screenPos')
self.voxXPos = gl.glGetAttribLocation( self.shaders, 'voxX')
self.voxYPos = gl.glGetAttribLocation( self.shaders, 'voxY')
self.voxZPos = gl.glGetAttribLocation( self.shaders, 'voxZ')
# Initialise data for the images that # Initialise data for the images that
# are already in the image list # are already in the image list
self._imageListChanged() self._imageListChanged()
# A bit hacky. We can only set the GL context (and create
# the GL data) once something is actually displayed on the
# screen. The _initGLData method is called (asynchronously)
# by the draw() method if it sees that the glReady flag has
# not yet been set. But draw() may be called mored than once
# before _initGLData is called. Here, to prevent
# _initGLData from running more than once, the first time
# it is called it simply overwrites itself with a dummy method.
self._initGLData = lambda s: s
self.glReady = True self.glReady = True
...@@ -590,7 +578,7 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -590,7 +578,7 @@ class SliceCanvas(wxgl.GLCanvas):
""" """
size = self.GetSize() size = self.GetSize()
# set up 2D drawing # set up 2D drawing
gl.glViewport(0, 0, size.width, size.height) gl.glViewport(0, 0, size.width, size.height)
gl.glMatrixMode(gl.GL_PROJECTION) gl.glMatrixMode(gl.GL_PROJECTION)
...@@ -616,6 +604,7 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -616,6 +604,7 @@ class SliceCanvas(wxgl.GLCanvas):
# clear the canvas # clear the canvas
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
# load the shaders
gl.glUseProgram(self.shaders) gl.glUseProgram(self.shaders)
# enable transparency # enable transparency
...@@ -637,17 +626,16 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -637,17 +626,16 @@ class SliceCanvas(wxgl.GLCanvas):
imageDisplay = image.display imageDisplay = image.display
geomBuffer = glImageData.geomBuffer dataBuffer = glImageData.dataBuffer
imageBuffer = glImageData.imageBuffer voxXBuffer = glImageData.voxXBuffer
positionBuffer = glImageData.positionBuffer voxYBuffer = glImageData.voxYBuffer
colourBuffer = glImageData.colourBuffer voxZBuffer = glImageData.voxZBuffer
geomBuffer = glImageData.geomBuffer
xdim = glImageData.xdim screenPosBuffer = glImageData.screenPosBuffer
ydim = glImageData.ydim
zdim = glImageData.zdim xdim = glImageData.xdim
xstride = glImageData.xstride ydim = glImageData.ydim
ystride = glImageData.ystride zdim = glImageData.zdim
zstride = glImageData.zstride
# Don't draw the slice if this # Don't draw the slice if this
# image display is disabled # image display is disabled
...@@ -656,79 +644,95 @@ class SliceCanvas(wxgl.GLCanvas): ...@@ -656,79 +644,95 @@ class SliceCanvas(wxgl.GLCanvas):
# Figure out which slice we are drawing, # Figure out which slice we are drawing,
# and if it's out of range, don't draw it # and if it's out of range, don't draw it
zi = int(image.worldToVox(self.zpos, self.zax)) zi = int(image.worldToVox(self.zpos, self.zax))
if zi < 0 or zi >= zdim: continue if zi < 0 or zi >= zdim: continue
# Set up the colour buffer voxOffs = [0, 0, 0]
gl.glEnable(gl.GL_TEXTURE_1D) voxSteps = [1, 1, 1]
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer)
gl.glUniform1i(self.colourMapPos, 0)
voxOffs[ self.zax] = zi
voxSteps[self.yax] = xdim
voxSteps[self.zax] = xdim * ydim
# bind the current alpha value to the
# shader alpha variable
gl.glUniform1f(self.alphaPos, imageDisplay.alpha) gl.glUniform1f(self.alphaPos, imageDisplay.alpha)
# We draw each horizontal row of voxels one at a time. # bind the transformation matrix
# This is necessary because, in order to allow image # to the shader variable
# buffers to be shared between different SliceCanvas # gl.glUniformMatrix4fv(self.voxToWorldMatPos,
# objects, we cannot re-arrange the image data, as # 1, False, transformBuffer)
# stored in GPU memory. So while the memory offset
# between values in the same row (or column) is
# consistent, the offset between rows (columns) is
# not. And drawing rows seems to be faster than
# drawing columns, for reasons unknown to me.
for yi in range(ydim):
imageOffset = zi * zstride + yi * ystride
imageStride = xstride
posOffset = yi * xdim * 8
# The geometry buffer, which defines the geometry of a
# single vertex (4 vertices, drawn as a triangle strip)
geomBuffer.bind()
gl.glVertexAttribPointer(
self.inVertexPos,
2,
gl.GL_FLOAT,
gl.GL_FALSE,
0,
None)
gl.glEnableVertexAttribArray(self.inVertexPos)
arbia.glVertexAttribDivisorARB(self.inVertexPos, 0)
# The position buffer, which defines # Set up the colour buffer
# the location of every voxel # gl.glEnable(gl.GL_TEXTURE_1D)
positionBuffer.bind() # gl.glActiveTexture(gl.GL_TEXTURE0)
# gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer)
# gl.glUniform1i(self.colourMapPos, 0)
# Set up the image data buffer
gl.glEnable(gl.GL_TEXTURE_3D)
# change to texxture 1 when you get working
gl.glActiveTexture(gl.GL_TEXTURE0)
gl.glBindTexture(gl.GL_TEXTURE_3D, dataBuffer)
gl.glUniform1i(self.dataBufferPos, 0)
# Screen x positions
screenPosBuffer.bind()
gl.glVertexAttribPointer(
self.screenPosPos,
2,
gl.GL_FLOAT,
gl.GL_FALSE,
0,
None)
gl.glEnableVertexAttribArray(self.screenPosPos)
arbia.glVertexAttribDivisorARB(self.screenPosPos, 1)
for buf, pos, step, off in zip(
(voxXBuffer, voxYBuffer, voxZBuffer),
(self.voxXPos, self.voxYPos, self.voxZPos),
voxSteps,
voxOffs):
buf.bind()
gl.glVertexAttribPointer( gl.glVertexAttribPointer(
self.inPositionPos, pos,
2, 1,
gl.GL_FLOAT, gl.GL_FLOAT,
gl.GL_FALSE, gl.GL_FALSE,
0, 0,
positionBuffer + posOffset) buf + off * 4)
gl.glEnableVertexAttribArray(self.inPositionPos) gl.glEnableVertexAttribArray(pos)
arbia.glVertexAttribDivisorARB(self.inPositionPos, 1) arbia.glVertexAttribDivisorARB(pos, step)
# The image buffer, which defines # The geometry buffer, which defines the geometry of a
# the colour value at each voxel. # single vertex (4 vertices, drawn as a triangle strip)
imageBuffer.bind() geomBuffer.bind()
gl.glVertexAttribPointer( gl.glVertexAttribPointer(
self.voxelValuePos, self.inVertexPos,
1, 2,
gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT,
gl.GL_TRUE, gl.GL_FALSE,
imageStride, 0,
imageBuffer + imageOffset) None)
gl.glEnableVertexAttribArray(self.inVertexPos)
gl.glEnableVertexAttribArray(self.voxelValuePos) arbia.glVertexAttribDivisorARB(self.inVertexPos, 0)
arbia.glVertexAttribDivisorARB(self.voxelValuePos, 1)
# Draw all of the triangles! print 'Draw {} voxels from slice {} ({} {} {})'.format(
arbdi.glDrawArraysInstancedARB( xdim * ydim, zi, self.xax, self.yax, self.zax)
gl.GL_TRIANGLE_STRIP, 0, 4, xdim) print "Offsets: {}".format(voxOffs)
print "Steps: {}".format(voxSteps)
gl.glDisableVertexAttribArray(self.inVertexPos)
gl.glDisableVertexAttribArray(self.inPositionPos) arbdi.glDrawArraysInstancedARB(
gl.glDisableVertexAttribArray(self.voxelValuePos) gl.GL_TRIANGLE_STRIP, 0, 4, xdim * ydim)
gl.glDisable(gl.GL_TEXTURE_1D)
gl.glDisableVertexAttribArray(self.inVertexPos)
gl.glDisableVertexAttribArray(self.screenPosPos)
gl.glDisableVertexAttribArray(self.voxXPos)
gl.glDisableVertexAttribArray(self.voxYPos)
gl.glDisableVertexAttribArray(self.voxZPos)
gl.glDisable(gl.GL_TEXTURE_1D)
gl.glDisable(gl.GL_TEXTURE_3D)
gl.glUseProgram(0) gl.glUseProgram(0)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment