Parcourir la source

First pass at implementing anonymous UBOs in OpenGL

rdb il y a 10 ans
Parent
commit
5e45d4f548

+ 86 - 1
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -346,7 +346,8 @@ int CLP(GraphicsStateGuardian)::get_driver_shader_version_minor() { return _gl_s
 CLP(GraphicsStateGuardian)::
 CLP(GraphicsStateGuardian)(GraphicsEngine *engine, GraphicsPipe *pipe) :
   GraphicsStateGuardian(gl_coordinate_system, engine, pipe),
-  _renderbuffer_residency(get_prepared_objects()->get_name(), "renderbuffer")
+  _renderbuffer_residency(get_prepared_objects()->get_name(), "renderbuffer"),
+  _ubuffer_residency(get_prepared_objects()->get_name(), "ubuffer")
 {
   _error_count = 0;
   _gl_shadlang_ver_major = 0;
@@ -2473,6 +2474,8 @@ reset() {
 
   _current_vbuffer_index = 0;
   _current_ibuffer_index = 0;
+  _current_ubuffer_index = 0;
+  _current_ubuffer_base.clear();
   _current_vao_index = 0;
   _current_fbo = 0;
   _auto_antialias_mode = false;
@@ -4873,6 +4876,88 @@ release_shader(ShaderContext *sc) {
   delete sc;
 }
 
+#ifndef OPENGLES
+////////////////////////////////////////////////////////////////////
+//     Function: GLGraphicsStateGuardian::get_uniform_buffer
+//       Access: Public, Virtual
+//  Description: Returns a uniform buffer matching the given buffer
+//               layout.  This is done so that we can can share
+//               similar buffers between different shaders that use
+//               them.
+////////////////////////////////////////////////////////////////////
+PT(CLP(UniformBufferContext)) CLP(GraphicsStateGuardian)::
+get_uniform_buffer(const GeomVertexArrayFormat *layout) {
+  nassertr(_supports_uniform_buffers, NULL);
+
+  UniformBuffers::const_iterator it = _uniform_buffers.find(layout);
+  if (it != _uniform_buffers.end()) {
+    // We already have an existing uniform buffer with the same layout.
+    return (*it).second;
+  }
+
+  // Create a new uniform buffer.
+  PT(CLP(UniformBufferContext)) ubc = new CLP(UniformBufferContext)(this, layout);
+  _glGenBuffers(1, &ubc->_index);
+  GLsizeiptr size = layout->get_pad_to();
+
+  if (GLCAT.is_debug()) {
+    GLCAT.debug() << "creating new uniform buffer " << ubc->_index
+                  << " with size " << size << "\n";
+  }
+
+  // Create the buffer and initialize its storage, but don't put
+  // anything in it just yet.
+  _glBindBuffer(GL_UNIFORM_BUFFER, ubc->_index);
+  _current_ubuffer_index = ubc->_index;
+
+  // Get immutable buffer storage if we support it, but onl if we
+  // can efficiently invalidate it before mapping it.
+  if (_supports_buffer_storage && _glMapBufferRange != NULL) {
+    _glBufferStorage(GL_UNIFORM_BUFFER, size, NULL, GL_MAP_WRITE_BIT);
+  } else {
+    _glBufferData(GL_UNIFORM_BUFFER, size, NULL, GL_STREAM_DRAW);
+  }
+
+  ubc->update_data_size_bytes(size);
+  _uniform_buffers[layout] = ubc;
+  return ubc;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: GLGraphicsStateGuardian::apply_uniform_buffer
+//       Access: Public
+//  Description: Makes the data the currently available data for
+//               rendering.
+////////////////////////////////////////////////////////////////////
+bool CLP(GraphicsStateGuardian)::
+apply_uniform_buffer(int index, CLP(UniformBufferContext) *gubc,
+                     const ShaderAttrib *attrib) {
+  nassertr(_supports_uniform_buffers, false);
+
+  if (index >= _current_ubuffer_base.size()) {
+    _current_ubuffer_base.resize(index + 1, 0);
+  }
+
+  if (_current_ubuffer_base[index] != gubc->_index) {
+    if (GLCAT.is_spam() && gl_debug_buffers) {
+      GLCAT.spam()
+        << "binding uniform buffer " << (int)gubc->_index
+        << " to index " << index << "\n";
+    }
+    _glBindBufferBase(GL_UNIFORM_BUFFER, index, gubc->_index);
+    _current_ubuffer_base[index] = gubc->_index;
+    _current_ubuffer_index = gubc->_index;
+  }
+
+  gubc->set_active(true);
+  gubc->update_data(attrib);
+
+  report_my_gl_errors();
+  return true;
+}
+
+#endif  // !OPENGLES
+
 ////////////////////////////////////////////////////////////////////
 //     Function: GLGraphicsStateGuardian::record_deleted_display_list
 //       Access: Public

+ 14 - 0
panda/src/glstuff/glGraphicsStateGuardian_src.h

@@ -301,6 +301,12 @@ public:
   virtual ShaderContext *prepare_shader(Shader *shader);
   virtual void release_shader(ShaderContext *sc);
 
+#ifndef OPENGLES
+  PT(CLP(UniformBufferContext)) get_uniform_buffer(const GeomVertexArrayFormat *layout);
+  bool apply_uniform_buffer(int index, CLP(UniformBufferContext) *gubc,
+                            const ShaderAttrib *attrib);
+#endif
+
   void record_deleted_display_list(GLuint index);
 
   virtual VertexBufferContext *prepare_vertex_buffer(GeomVertexArrayData *data);
@@ -633,6 +639,8 @@ protected:
   GLuint _geom_display_list;
   GLuint _current_vbuffer_index;
   GLuint _current_ibuffer_index;
+  GLuint _current_ubuffer_index;
+  pvector<GLuint> _current_ubuffer_base;
   GLuint _current_fbo;
   int _num_active_texture_stages;
   PN_stdfloat _max_anisotropy;
@@ -911,6 +919,10 @@ public:
   TextureSet _textures_needing_image_access_barrier;
   TextureSet _textures_needing_update_barrier;
   TextureSet _textures_needing_framebuffer_barrier;
+
+  // Store uniform buffers.
+  typedef pmap<CPT(GeomVertexArrayFormat), CLP(UniformBufferContext)*> UniformBuffers;
+  UniformBuffers _uniform_buffers;
 #endif
 
   //RenderState::SlotMask _inv_state_mask;
@@ -942,6 +954,7 @@ public:
 #endif  // NDEBUG
 
   BufferResidencyTracker _renderbuffer_residency;
+  BufferResidencyTracker _ubuffer_residency;
 
   static PStatCollector _load_display_list_pcollector;
   static PStatCollector _primitive_batches_display_list_pcollector;
@@ -975,6 +988,7 @@ private:
 
   friend class CLP(VertexBufferContext);
   friend class CLP(IndexBufferContext);
+  friend class CLP(UniformBufferContext);
   friend class CLP(ShaderContext);
   friend class CLP(CgShaderContext);
   friend class CLP(GraphicsBuffer);

+ 31 - 22
panda/src/glstuff/glShaderContext_src.cxx

@@ -481,21 +481,14 @@ reflect_attribute(int i, char *name_buffer, GLsizei name_buflen) {
 ////////////////////////////////////////////////////////////////////
 void CLP(ShaderContext)::
 reflect_uniform_block(int i, const char *name, char *name_buffer, GLsizei name_buflen) {
- //GLint offset = 0;
-
   GLint data_size = 0;
   GLint param_count = 0;
-  GLsizei param_size;
   _glgsg->_glGetActiveUniformBlockiv(_glsl_program, i, GL_UNIFORM_BLOCK_DATA_SIZE, &data_size);
   _glgsg->_glGetActiveUniformBlockiv(_glsl_program, i, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &param_count);
 
-  if (param_count <= 0) {
-    return;
-  }
-
   // We use a GeomVertexArrayFormat to describe the uniform buffer layout.
-  //GeomVertexArrayFormat block_format;
-  //block_format.set_pad_to(data_size);
+  GeomVertexArrayFormat *block_format = new GeomVertexArrayFormat;
+  block_format->set_pad_to(data_size);
 
   // Get an array containing the indices of all the uniforms in this block.
   GLuint *indices = (GLuint *)alloca(param_count * sizeof(GLint));
@@ -612,23 +605,26 @@ reflect_uniform_block(int i, const char *name, char *name_buffer, GLsizei name_b
       break;
     }
 
-    //GeomVertexColumn column(InternalName::make(name_buffer),
-    //                        num_components, numeric_type, contents,
-    //                        offsets[ui], 4, param_size, astrides[ui]);
-    //block_format.add_column(column);
+    GeomVertexColumn column(InternalName::make(name_buffer),
+                            num_components, numeric_type, contents,
+                            offsets[ui], 4, param_size, astrides[ui]);
+    block_format->add_column(column);
+    cerr << ui << " = " << column << "\n";
   }
 
-  //if (GLCAT.is_debug()) {
-  //  GLCAT.debug() << "Active uniform block " << name << " has format:\n";
-  //  block_format.write(GLCAT.debug(false), 2);
-  //}
+  if (GLCAT.is_debug()) {
+    GLCAT.debug() << "Active uniform block " << name << " has layout:\n";
+    block_format->write(GLCAT.debug(false), 2);
+  }
+
+  CPT(GeomVertexArrayFormat) layout = GeomVertexArrayFormat::register_format(block_format);
 
-  //UniformBlock block;
-  //block._name = InternalName::make(name);
-  //block._format = GeomVertexArrayFormat::register_format(&block_format);
-  //block._buffer = 0;
+  UniformBlock block;
+  block._name = InternalName::make(name);
+  block._buffer = _glgsg->get_uniform_buffer(layout);
 
-  //_uniform_blocks.push_back(block);
+  nassertv(i == _uniform_blocks.size());
+  _uniform_blocks.push_back(block);
 }
 #endif  // !OPENGLES
 
@@ -1658,6 +1654,19 @@ issue_parameters(int altered) {
       << " (altered 0x" << hex << altered << dec << ")\n";
   }
 
+#ifndef OPENGLES
+  // Update uniform buffers.  No modification tracking is currently done.
+  if (altered & (Shader::SSD_shaderinputs | Shader::SSD_frame)) {
+    const ShaderAttrib *attrib;
+    _state_rs->get_attrib_def(attrib);
+
+    for (int i = 0; i < _uniform_blocks.size(); ++i) {
+      UniformBlock &block = _uniform_blocks[i];
+      _glgsg->apply_uniform_buffer(i, block._buffer, attrib);
+    }
+  }
+#endif
+
   // We have no way to track modifications to PTAs, so we assume that
   // they are modified every frame and when we switch ShaderAttribs.
   if (altered & (Shader::SSD_shaderinputs | Shader::SSD_frame)) {

+ 10 - 0
panda/src/glstuff/glShaderContext_src.h

@@ -90,14 +90,24 @@ private:
   GLsizei _slider_table_size;
   GLint _frame_number_loc;
   GLint _frame_number;
+
+#ifndef OPENGLES
   pmap<GLint, GLuint64> _glsl_uniform_handles;
 
+  struct UniformBlock {
+    CPT(InternalName) _name;
+    PT(CLP(UniformBufferContext)) _buffer;
+  };
+  typedef pvector<UniformBlock> UniformBlocks;
+  UniformBlocks _uniform_blocks;
+
   struct ImageInput {
     CPT(InternalName) _name;
     CLP(TextureContext) *_gtc;
     bool _writable;
   };
   pvector<ImageInput> _glsl_img_inputs;
+#endif
 
   CLP(GraphicsStateGuardian) *_glgsg;
 

+ 14 - 0
panda/src/glstuff/glUniformBufferContext_src.I

@@ -0,0 +1,14 @@
+// Filename: glUniformBufferContext_src.I
+// Created by:  rdb (29Jul15)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+

+ 128 - 0
panda/src/glstuff/glUniformBufferContext_src.cxx

@@ -0,0 +1,128 @@
+// Filename: glUniformBufferContext_src.cxx
+// Created by:  rdb (29Jul15)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifndef OPENGLES
+
+TypeHandle CLP(UniformBufferContext)::_type_handle;
+
+////////////////////////////////////////////////////////////////////
+//     Function: CLP(UniformBufferContext)::Constructor
+//       Access: Public
+//  Description:
+////////////////////////////////////////////////////////////////////
+CLP(UniformBufferContext)::
+CLP(UniformBufferContext)(CLP(GraphicsStateGuardian) *glgsg,
+                          CPT(GeomVertexArrayFormat) layout) :
+  BufferContext(&glgsg->_ubuffer_residency),
+  _glgsg(glgsg),
+  _layout(MOVE(layout)),
+  _index(0),
+  _mat_deps(0)
+{
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: GLUniformBufferContext::Destructor
+//       Access: Public
+//  Description:
+////////////////////////////////////////////////////////////////////
+CLP(UniformBufferContext)::
+~CLP(UniformBufferContext)() {
+  // TODO: unregister uniform buffer from GSG.
+  if (_index != 0) {
+    _glgsg->_glDeleteBuffers(1, &_index);
+    _index = 0;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: GLUniformBufferContext::update
+//       Access: Public
+//  Description:
+////////////////////////////////////////////////////////////////////
+void CLP(UniformBufferContext)::
+update_data(const ShaderAttrib *attrib) {
+  //if (ClockObject::get_global_clock()->get_frame_count() == _frame) {
+  //  return;
+  //}
+  _frame = ClockObject::get_global_clock()->get_frame_count();
+
+  // Change the generic buffer binding target for the map operation.
+  if (_glgsg->_current_ubuffer_index != _index) {
+    _glgsg->_glBindBuffer(GL_UNIFORM_BUFFER, _index);
+    _glgsg->_current_ubuffer_index = _index;
+  }
+
+  void *buffer;
+  GLsizeiptr size = _layout->get_pad_to();
+
+  if (_glgsg->_glMapBufferRange != NULL) {
+    // If we have glMapBufferRange, we can tell it we don't need the
+    // previous contents for future draw calls any more, preventing
+    // an unnecessary synchronization.
+    buffer = _glgsg->_glMapBufferRange(GL_UNIFORM_BUFFER, 0, size,
+      GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
+
+  } else {
+    // This old trick achieves more or less the same effect.
+    _glgsg->_glBufferData(GL_UNIFORM_BUFFER, size, NULL, GL_STREAM_DRAW);
+    buffer = _glgsg->_glMapBuffer(GL_UNIFORM_BUFFER, GL_WRITE_ONLY);
+  }
+
+  nassertv(buffer != NULL);
+
+  for (int i = 0; i < _layout->get_num_columns(); ++i) {
+    const GeomVertexColumn *column = _layout->get_column(i);
+    const InternalName *name = column->get_name();
+
+    const ShaderInput *input = attrib->get_shader_input(name);
+    if (input == NULL || input->get_value_type() == ShaderInput::M_invalid) {
+      GLCAT.error()
+        << "Missing shader input: " << *name << "\n";
+      continue;
+    }
+
+    switch (column->get_numeric_type()) {
+    case GeomEnums::NT_float32:
+      {
+        float *into = (float *)((char *)buffer + column->get_start());
+        nassertd(input->extract_data(into, column->get_num_values(), column->get_num_elements())) continue;
+      }
+      break;
+
+    case GeomEnums::NT_float64:
+      {
+        double *into = (double *)((char *)buffer + column->get_start());
+        nassertd(input->extract_data(into, column->get_num_values(), column->get_num_elements())) continue;
+      }
+      break;
+
+    //TODO: support unsigned int
+    case GeomEnums::NT_int32:
+    case GeomEnums::NT_uint32:
+      {
+        int *into = (int *)((char *)buffer + column->get_start());
+        nassertd(input->extract_data(into, column->get_num_values(), column->get_num_elements())) continue;
+      }
+      break;
+
+    default:
+      continue;
+    }
+  }
+
+  _glgsg->_glUnmapBuffer(GL_UNIFORM_BUFFER);
+}
+
+#endif

+ 68 - 0
panda/src/glstuff/glUniformBufferContext_src.h

@@ -0,0 +1,68 @@
+// Filename: glUniformBufferContext_src.h
+// Created by:  rdb (29Jul15)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifndef OPENGLES
+
+#include "pandabase.h"
+#include "bufferContext.h"
+#include "deletedChain.h"
+#include "shader.h"
+
+////////////////////////////////////////////////////////////////////
+//       Class : GLUniformBufferContext
+// Description : Caches a GeomPrimitive on the GL as a buffer
+//               object.
+////////////////////////////////////////////////////////////////////
+class EXPCL_GL CLP(UniformBufferContext) : public BufferContext, public ReferenceCount {
+public:
+  CLP(UniformBufferContext)(CLP(GraphicsStateGuardian) *glgsg,
+                            CPT(GeomVertexArrayFormat) layout);
+  virtual ~CLP(UniformBufferContext)();
+
+  ALLOC_DELETED_CHAIN(CLP(UniformBufferContext));
+
+  CLP(GraphicsStateGuardian) *_glgsg;
+
+  // This is the GL "name" of the data object.
+  GLuint _index;
+
+  void update_data(const ShaderAttrib *attrib);
+
+private:
+  CPT(GeomVertexArrayFormat) _layout;
+  //Shader::ShaderMatSpecs _mat_spec;
+  int _mat_deps;
+  int _frame;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    BufferContext::init_type();
+    register_type(_type_handle, CLASSPREFIX_QUOTED "UniformBufferContext",
+                  BufferContext::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "glUniformBufferContext_src.I"
+
+#endif  // !OPENGLES

+ 1 - 0
panda/src/glstuff/glstuff_src.cxx

@@ -27,6 +27,7 @@
 #include "glLatencyQueryContext_src.cxx"
 #include "glGeomContext_src.cxx"
 #include "glGeomMunger_src.cxx"
+#include "glUniformBufferContext_src.cxx"
 #include "glShaderContext_src.cxx"
 #include "glCgShaderContext_src.cxx"
 #include "glImmediateModeSender_src.cxx"

+ 1 - 0
panda/src/glstuff/glstuff_src.h

@@ -41,6 +41,7 @@
 #include "glLatencyQueryContext_src.h"
 #include "glGeomContext_src.h"
 #include "glGeomMunger_src.h"
+#include "glUniformBufferContext_src.h"
 #include "glShaderContext_src.h"
 #include "glCgShaderContext_src.h"
 #include "glImmediateModeSender_src.h"

+ 138 - 0
panda/src/pgraph/shaderInput.cxx

@@ -121,6 +121,144 @@ get_sampler() const {
     : get_texture()->get_default_sampler();
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: ParamValue::extract_data
+//       Access: Public, Virtual
+//  Description: This is a convenience function for the graphics
+//               backend.  It can be used for numeric types to
+//               convert this value to an array of ints matching
+//               the given description.  If this is not possible,
+//               returns false (after which the array may or may not
+//               contain meaningful data).
+////////////////////////////////////////////////////////////////////
+bool ShaderInput::
+extract_data(PN_int32 *data, int width, size_t count) const {
+  switch (_type) {
+  case M_numeric:
+    if (_stored_ptr._type == Shader::SPT_int) {
+      int size = min(count * width, _stored_ptr._size);
+
+      for (int i = 0; i < size; ++i) {
+        data[i] = ((int *)_stored_ptr._ptr)[i];
+      }
+      return true;
+    }
+    return false;
+
+  default:
+    return false;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: ParamValue::extract_data
+//       Access: Public, Virtual
+//  Description: This is a convenience function for the graphics
+//               backend.  It can be used for numeric types to
+//               convert this value to an array of floats matching
+//               the given description.  If this is not possible,
+//               returns false (after which the array may or may not
+//               contain meaningful data).
+////////////////////////////////////////////////////////////////////
+bool ShaderInput::
+extract_data(PN_float32 *data, int width, size_t count) const {
+  int size;
+
+  switch (_type) {
+  case M_vector:
+    nassertr(width > 0 && width < 4, false);
+    for (int i = 0; i < width; ++i) {
+      data[i] = (float)_stored_vector[i];
+    }
+    return (count == 1);
+
+  case M_numeric:
+    size = min(count * width, _stored_ptr._size);
+
+    switch (_stored_ptr._type) {
+    case Shader::SPT_int:
+      for (int i = 0; i < size; ++i) {
+        int value = ((int *)_stored_ptr._ptr)[i];
+        data[i] = (float)value;
+      }
+      return true;
+
+    case Shader::SPT_float:
+      for (int i = 0; i < size; ++i) {
+        float value = ((float *)_stored_ptr._ptr)[i];
+        data[i] = (float)value;
+      }
+      return true;
+
+    case Shader::SPT_double:
+      for (int i = 0; i < size; ++i) {
+        double value = ((double *)_stored_ptr._ptr)[i];
+        data[i] = (float)value;
+      }
+      return true;
+
+    default:
+      return false;
+    }
+
+  default:
+    return false;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: ParamValue::extract_data
+//       Access: Public, Virtual
+//  Description: This is a convenience function for the graphics
+//               backend.  It can be used for numeric types to
+//               convert this value to an array of doubles matching
+//               the given description.  If this is not possible,
+//               returns false (after which the array may or may not
+//               contain meaningful data).
+////////////////////////////////////////////////////////////////////
+bool ShaderInput::
+extract_data(PN_float64 *data, int width, size_t count) const {
+  int size;
+
+  switch (_type) {
+  case M_vector:
+    nassertr(width > 0 && width < 4, false);
+    for (int i = 0; i < width; ++i) {
+      data[i] = (double)_stored_vector[i];
+    }
+    return (count == 1);
+
+  case M_numeric:
+    size = min(count * width, _stored_ptr._size);
+
+    switch (_stored_ptr._type) {
+    case Shader::SPT_int:
+      for (int i = 0; i < size; ++i) {
+        data[i] = (double)(((int *)_stored_ptr._ptr)[i]);
+      }
+      return true;
+
+    case Shader::SPT_float:
+      for (int i = 0; i < size; ++i) {
+        data[i] = (double)(((float *)_stored_ptr._ptr)[i]);
+      }
+      return true;
+
+    case Shader::SPT_double:
+      for (int i = 0; i < size; ++i) {
+        data[i] = (double)(((double *)_stored_ptr._ptr)[i]);
+      }
+      return true;
+
+    default:
+      return false;
+    }
+
+  default:
+    return false;
+  }
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: ShaderInput::register_with_read_factory
 //       Access: Public, Static

+ 4 - 0
panda/src/pgraph/shaderInput.h

@@ -112,6 +112,10 @@ PUBLISHED:
   Texture *get_texture() const;
   const SamplerState &get_sampler() const;
 
+  bool extract_data(PN_int32 *data, int width, size_t count) const;
+  bool extract_data(PN_float32 *data, int width, size_t count) const;
+  bool extract_data(PN_float64 *data, int width, size_t count) const;
+
 public:
   INLINE ParamValueBase *get_param() const;