// Filename: geomVertexData.cxx
// Created by:  drose (06Mar05)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc.  All rights reserved
//
// All use of this software is subject to the terms of the Panda 3d
// Software license.  You should have received a copy of this license
// along with this source code; you will also find a current copy of
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
//
// To contact the maintainers of this program write to
// panda3d-general@lists.sourceforge.net .
//
////////////////////////////////////////////////////////////////////

#include "geomVertexData.h"
#include "geom.h"
#include "geomVertexReader.h"
#include "geomVertexWriter.h"
#include "geomVertexRewriter.h"
#include "pStatTimer.h"
#include "bamReader.h"
#include "bamWriter.h"
#include "pset.h"
#include "indent.h"

TypeHandle GeomVertexData::_type_handle;
TypeHandle GeomVertexData::CDataCache::_type_handle;
TypeHandle GeomVertexData::CacheEntry::_type_handle;
TypeHandle GeomVertexData::CData::_type_handle;
TypeHandle GeomVertexDataPipelineReader::_type_handle;
TypeHandle GeomVertexDataPipelineWriter::_type_handle;

PStatCollector GeomVertexData::_convert_pcollector("*:Munge:Convert");
PStatCollector GeomVertexData::_scale_color_pcollector("*:Munge:Scale color");
PStatCollector GeomVertexData::_set_color_pcollector("*:Munge:Set color");
PStatCollector GeomVertexData::_animation_pcollector("*:Animation");

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::Default Constructor
//       Access: Private
//  Description: Constructs an invalid object.  This is only used when
//               reading from the bam file.
////////////////////////////////////////////////////////////////////
GeomVertexData::
GeomVertexData() :
  _char_pcollector(_animation_pcollector, "unnamed"),
  _skinning_pcollector(_char_pcollector, "Skinning"),
  _morphs_pcollector(_char_pcollector, "Morphs")
{
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::make_cow_copy
//       Access: Protected, Virtual
//  Description: Required to implement CopyOnWriteObject.
////////////////////////////////////////////////////////////////////
PT(CopyOnWriteObject) GeomVertexData::
make_cow_copy() {
  return new GeomVertexData(*this);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::Constructor
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
GeomVertexData::
GeomVertexData(const string &name,
               const GeomVertexFormat *format,
               GeomVertexData::UsageHint usage_hint) :
  _name(name),
  _char_pcollector(PStatCollector(_animation_pcollector, name)),
  _skinning_pcollector(_char_pcollector, "Skinning"),
  _morphs_pcollector(_char_pcollector, "Morphs")
{
  nassertv(format->is_registered());

  // Create some empty arrays as required by the format.
  // Let's ensure the vertex data gets set on all stages at once.
  OPEN_ITERATE_ALL_STAGES(_cycler) {
    CDStageWriter cdata(_cycler, pipeline_stage);
    cdata->_format = format;
    cdata->_usage_hint = usage_hint;
    int num_arrays = format->get_num_arrays();
    for (int i = 0; i < num_arrays; i++) {
      PT(GeomVertexArrayData) array = new GeomVertexArrayData
        (format->get_array(i), usage_hint);
      cdata->_arrays.push_back(array.p());
    }
  }
  CLOSE_ITERATE_ALL_STAGES(_cycler);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::Copy Constructor
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
GeomVertexData::
GeomVertexData(const GeomVertexData &copy) :
  CopyOnWriteObject(copy),
  _name(copy._name),
  _cycler(copy._cycler),
  _char_pcollector(copy._char_pcollector),
  _skinning_pcollector(copy._skinning_pcollector),
  _morphs_pcollector(copy._morphs_pcollector)
{
  OPEN_ITERATE_ALL_STAGES(_cycler) {
    CDStageWriter cdata(_cycler, pipeline_stage);
    // It's important that we *not* copy the animated_vertices pointer.
    cdata->_animated_vertices = NULL;
    cdata->_animated_vertices_modified = UpdateSeq();
  }
  CLOSE_ITERATE_ALL_STAGES(_cycler);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::Constructor
//       Access: Published
//  Description: This constructor copies all of the basic properties
//               of the source VertexData, like usage_hint and
//               animation tables, but does not copy the actual data,
//               and it allows you to specify a different format.
////////////////////////////////////////////////////////////////////
GeomVertexData::
GeomVertexData(const GeomVertexData &copy, 
               const GeomVertexFormat *format) :
  CopyOnWriteObject(copy),
  _name(copy._name),
  _cycler(copy._cycler),
  _char_pcollector(copy._char_pcollector),
  _skinning_pcollector(copy._skinning_pcollector),
  _morphs_pcollector(copy._morphs_pcollector)
{
  nassertv(format->is_registered());

  // Create some empty arrays as required by the format.
  OPEN_ITERATE_ALL_STAGES(_cycler) {
    CDStageWriter cdata(_cycler, pipeline_stage);

    UsageHint usage_hint = cdata->_usage_hint;
    cdata->_arrays.clear();
    cdata->_format = format;
    int num_arrays = format->get_num_arrays();
    for (int i = 0; i < num_arrays; i++) {
      PT(GeomVertexArrayData) array = new GeomVertexArrayData
        (format->get_array(i), usage_hint);
      cdata->_arrays.push_back(array.p());
    }

    // It's important that we *not* copy the animated_vertices pointer.
    cdata->_animated_vertices = NULL;
    cdata->_animated_vertices_modified = UpdateSeq();
  }
  CLOSE_ITERATE_ALL_STAGES(_cycler);
}
  
////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::Copy Assignment Operator
//       Access: Published
//  Description: The copy assignment operator is not pipeline-safe.
//               This will completely obliterate all stages of the
//               pipeline, so don't do it for a GeomVertexData that is
//               actively being used for rendering.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
operator = (const GeomVertexData &copy) {
  CopyOnWriteObject::operator = (copy);

  clear_cache();

  _name = copy._name;
  _cycler = copy._cycler;
  _char_pcollector = copy._char_pcollector;
  _skinning_pcollector = copy._skinning_pcollector;
  _morphs_pcollector = copy._morphs_pcollector;

  OPEN_ITERATE_ALL_STAGES(_cycler) {
    CDStageWriter cdata(_cycler, pipeline_stage);
    cdata->_modified = Geom::get_next_modified();
    cdata->_animated_vertices = NULL;
    cdata->_animated_vertices_modified = UpdateSeq();
  } 
  CLOSE_ITERATE_ALL_STAGES(_cycler);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::Destructor
//       Access: Published, Virtual
//  Description: 
////////////////////////////////////////////////////////////////////
GeomVertexData::
~GeomVertexData() {
  clear_cache();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::compare_to
//       Access: Published
//  Description: Returns 0 if the two objects are equivalent, even if
//               they are not the same pointer.
////////////////////////////////////////////////////////////////////
int GeomVertexData::
compare_to(const GeomVertexData &other) const {
  Thread *current_thread = Thread::get_current_thread();

  CDReader cdata(_cycler);
  CDReader other_cdata(other._cycler);

  if (cdata->_usage_hint != other_cdata->_usage_hint) {
    return (int)cdata->_usage_hint - (int)other_cdata->_usage_hint;
  }
  if (cdata->_format != other_cdata->_format) {
    return cdata->_format < other_cdata->_format ? -1 : 1;
  }
  if (cdata->_transform_table != other_cdata->_transform_table) {
    return cdata->_transform_table < other_cdata->_transform_table ? -1 : 1;
  }
  if (cdata->_transform_blend_table != other_cdata->_transform_blend_table) {
    return cdata->_transform_blend_table < other_cdata->_transform_blend_table ? -1 : 1;
  }
  if (cdata->_slider_table != other_cdata->_slider_table) {
    return cdata->_slider_table < other_cdata->_slider_table ? -1 : 1;
  }
  if (cdata->_arrays.size() != other_cdata->_arrays.size()) {
    return (int)cdata->_arrays.size() - (int)other_cdata->_arrays.size();
  }
  for (size_t i = 0; i < cdata->_arrays.size(); ++i) {
    if (cdata->_arrays[i] != other_cdata->_arrays[i]) {
      return cdata->_arrays[i] < other_cdata->_arrays[i] ? -1 : 1;
    }
  }

  return 0;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_name
//       Access: Published
//  Description: Changes the name of the vertex data.  This name is
//               reported on the PStats graph for vertex computations.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
set_name(const string &name) {
  _name = name;
  _char_pcollector = PStatCollector(_animation_pcollector, name);
  _skinning_pcollector = PStatCollector(_char_pcollector, "Skinning");
  _morphs_pcollector = PStatCollector(_char_pcollector, "Morphs");
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_usage_hint
//       Access: Published
//  Description: Changes the UsageHint hint for this vertex data, and
//               for all of the arrays that share this data.  See
//               get_usage_hint().
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
set_usage_hint(GeomVertexData::UsageHint usage_hint) {
  CDWriter cdata(_cycler, true);
  cdata->_usage_hint = usage_hint;

  Arrays::iterator ai;
  for (ai = cdata->_arrays.begin();
       ai != cdata->_arrays.end();
       ++ai) {
    PT(GeomVertexArrayData) array_obj = (*ai).get_write_pointer();
    array_obj->set_usage_hint(usage_hint);
  }
  clear_cache_stage();
  cdata->_modified = Geom::get_next_modified();
  cdata->_animated_vertices_modified = UpdateSeq();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_format
//       Access: Published
//  Description: Changes the format of the vertex data.  If the data
//               is not empty, this will implicitly change every row
//               to match the new format.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
set_format(const GeomVertexFormat *format) {
  Thread *current_thread = Thread::get_current_thread();
  nassertv(format->is_registered());

  CDLockedReader cdata(_cycler, current_thread);

  if (format == cdata->_format) {
    // Trivially no-op.
    return;
  }

  CDWriter cdataw(_cycler, cdata, true);

  // Put the current data aside, so we can copy it back in below.
  CPT(GeomVertexData) orig_data = new GeomVertexData(*this);

  // Assign the new format.  This means clearing out all of our
  // current arrays and replacing them with new, empty arrays.
  cdataw->_format = format;

  UsageHint usage_hint = cdataw->_usage_hint;
  cdataw->_arrays.clear();
  int num_arrays = cdataw->_format->get_num_arrays();
  for (int i = 0; i < num_arrays; i++) {
    PT(GeomVertexArrayData) array = new GeomVertexArrayData
      (cdataw->_format->get_array(i), usage_hint);
    cdataw->_arrays.push_back(array.p());
  }

  // Now copy the original data back in.  This will automatically
  // convert it to the new format.
  copy_from(orig_data, false, current_thread);

  clear_cache_stage();
  cdataw->_modified = Geom::get_next_modified();
  cdataw->_animated_vertices.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::clear_rows
//       Access: Published
//  Description: Removes all of the rows from the arrays;
//               functionally equivalent to set_num_rows(0) (but
//               faster).
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
clear_rows() {
  Thread *current_thread = Thread::get_current_thread();
  CDWriter cdata(_cycler, true, current_thread);
  nassertv(cdata->_format->get_num_arrays() == (int)cdata->_arrays.size());

  Arrays::iterator ai;
  for (ai = cdata->_arrays.begin();
       ai != cdata->_arrays.end();
       ++ai) {
    PT(GeomVertexArrayData) array_obj = (*ai).get_write_pointer();
    array_obj->clear_rows();
  }
  clear_cache_stage();
  cdata->_modified = Geom::get_next_modified();
  cdata->_animated_vertices.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_transform_table
//       Access: Published
//  Description: Replaces the TransformTable on this vertex
//               data with the indicated table.  The length of this
//               table should be consistent with the maximum table
//               index assigned to the vertices under the
//               "transform_index" name.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
set_transform_table(const TransformTable *table) {
  Thread *current_thread = Thread::get_current_thread();
  nassertv(table == (TransformTable *)NULL || table->is_registered());

  CDWriter cdata(_cycler, true, current_thread);
  cdata->_transform_table = (TransformTable *)table;
  clear_cache_stage();
  cdata->_modified = Geom::get_next_modified();
  cdata->_animated_vertices_modified = UpdateSeq();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::modify_transform_blend_table
//       Access: Published
//  Description: Returns a modifiable pointer to the current
//               TransformBlendTable on this vertex data, if any, or
//               NULL if there is not a TransformBlendTable.  See
//               get_transform_blend_table().
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
PT(TransformBlendTable) GeomVertexData::
modify_transform_blend_table() {
  CDWriter cdata(_cycler, true);

  clear_cache_stage();
  cdata->_modified = Geom::get_next_modified();
  cdata->_animated_vertices_modified = UpdateSeq();

  return cdata->_transform_blend_table.get_write_pointer();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_transform_blend_table
//       Access: Published
//  Description: Replaces the TransformBlendTable on this vertex
//               data with the indicated table.  The length of this
//               table should be consistent with the maximum table
//               index assigned to the vertices under the
//               "transform_blend" name.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
set_transform_blend_table(const TransformBlendTable *table) {
  CDWriter cdata(_cycler, true);
  cdata->_transform_blend_table = (TransformBlendTable *)table;
  clear_cache_stage();
  cdata->_modified = Geom::get_next_modified();
  cdata->_animated_vertices_modified = UpdateSeq();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_slider_table
//       Access: Published
//  Description: Replaces the SliderTable on this vertex
//               data with the indicated table.  There should be an
//               entry in this table for each kind of morph offset
//               defined in the vertex data.
//
//               The SliderTable object must have been registered
//               prior to setting it on the GeomVertexData.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
set_slider_table(const SliderTable *table) {
  nassertv(table == (SliderTable *)NULL || table->is_registered());

  CDWriter cdata(_cycler, true);
  cdata->_slider_table = (SliderTable *)table;
  clear_cache_stage();
  cdata->_modified = Geom::get_next_modified();
  cdata->_animated_vertices_modified = UpdateSeq();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::request_resident
//       Access: Published
//  Description: Returns true if the vertex data is currently resident
//               in memory.  If this returns false, the vertex data will
//               be brought back into memory shortly; try again later.
////////////////////////////////////////////////////////////////////
bool GeomVertexData::
request_resident() const {
  CDReader cdata(_cycler);

  bool resident = true;

  Arrays::const_iterator ai;
  for (ai = cdata->_arrays.begin();
       ai != cdata->_arrays.end();
       ++ai) {
    if (!(*ai).get_read_pointer()->request_resident()) {
      resident = false;
    }
  }

  return resident;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::copy_from
//       Access: Published
//  Description: Copies all the data from the other array into the
//               corresponding data types in this array, by matching
//               data types name-by-name.
//
//               keep_data_objects specifies what to do when one or
//               more of the arrays can be copied without the need to
//               apply any conversion operation.  If it is true, the
//               original GeomVertexArrayData objects in this object
//               are retained, and their data arrays are copied
//               byte-by-byte from the source; if it is false, then the
//               GeomVertexArrayData objects are copied pointerwise
//               from the source.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
copy_from(const GeomVertexData *source, bool keep_data_objects,
          Thread *current_thread) {
  const GeomVertexFormat *source_format = source->get_format();
  const GeomVertexFormat *dest_format = get_format();

  int num_rows = source->get_num_rows();
  int num_arrays = source_format->get_num_arrays();
  int source_i;

  // First, check to see if any arrays can be simply appropriated for
  // the new format, without changing the data.
  pset<int> done_arrays;

  for (source_i = 0; source_i < num_arrays; ++source_i) {
    const GeomVertexArrayFormat *source_array_format = 
      source_format->get_array(source_i);

    bool array_done = false;

    int dest_num_arrays = dest_format->get_num_arrays();
    for (int dest_i = 0; 
         dest_i < dest_num_arrays && !array_done; 
         ++dest_i) {
      const GeomVertexArrayFormat *dest_array_format = 
        dest_format->get_array(dest_i);
      if (dest_array_format->is_data_subset_of(*source_array_format)) {
        // Great!  Just use the same data for this one.
        if (keep_data_objects) {
          // Copy the data, but keep the same GeomVertexArrayData object.  
          
          PT(GeomVertexArrayData) dest_data = modify_array(dest_i);
          CPT(GeomVertexArrayData) source_data = source->get_array(source_i);
          dest_data->modify_handle()->copy_data_from(source_data->get_handle());
        } else {
          // Copy the GeomVertexArrayData object.
          if (get_array(dest_i) != source->get_array(source_i)) {
            set_array(dest_i, source->get_array(source_i));
          }
        }

        array_done = true;
        done_arrays.insert(dest_i);
      }
    }
  }

  // Now make sure the arrays we didn't share are all filled in.
  set_num_rows(num_rows);

  // Now go back through and copy any data that's left over.
  for (source_i = 0; source_i < num_arrays; ++source_i) {
    CPT(GeomVertexArrayData) array_obj = source->get_array(source_i);
    CPT(GeomVertexArrayDataHandle) array_handle = array_obj->get_handle();
    const unsigned char *array_data = array_handle->get_read_pointer(true);
    const GeomVertexArrayFormat *source_array_format = source_format->get_array(source_i);
    int num_columns = source_array_format->get_num_columns();
    for (int di = 0; di < num_columns; ++di) {
      const GeomVertexColumn *source_column = source_array_format->get_column(di);

      int dest_i = dest_format->get_array_with(source_column->get_name());
      if (dest_i >= 0 && done_arrays.count(dest_i) == 0) {
        // The data type exists in the new format; we have to copy it.
        const GeomVertexArrayFormat *dest_array_format = 
          dest_format->get_array(dest_i);
        const GeomVertexColumn *dest_column = 
          dest_array_format->get_column(source_column->get_name());
        nassertv(dest_column != (const GeomVertexColumn *)NULL);

        if (dest_column->is_bytewise_equivalent(*source_column)) {
          // We can do a quick bytewise copy.
          PT(GeomVertexArrayData) dest_array_obj = modify_array(dest_i);
          PT(GeomVertexArrayDataHandle) dest_handle = dest_array_obj->modify_handle();
          unsigned char *dest_array_data = dest_handle->get_write_pointer();

          bytewise_copy(dest_array_data + dest_column->get_start(), 
                        dest_array_format->get_stride(),
                        array_data + source_column->get_start(), source_array_format->get_stride(),
                        source_column, num_rows);

        } else if (dest_column->is_packed_argb() && 
                   source_column->is_uint8_rgba()) {
          // A common special case: OpenGL color to DirectX color.
          PT(GeomVertexArrayData) dest_array_obj = modify_array(dest_i);
          PT(GeomVertexArrayDataHandle) dest_handle = dest_array_obj->modify_handle();
          unsigned char *dest_array_data = dest_handle->get_write_pointer();

          uint8_rgba_to_packed_argb
            (dest_array_data + dest_column->get_start(), 
             dest_array_format->get_stride(),
             array_data + source_column->get_start(), source_array_format->get_stride(),
             num_rows);

        } else if (dest_column->is_uint8_rgba() && 
                   source_column->is_packed_argb()) {
          // Another common special case: DirectX color to OpenGL
          // color.
          PT(GeomVertexArrayData) dest_array_obj = modify_array(dest_i);
          PT(GeomVertexArrayDataHandle) dest_handle = dest_array_obj->modify_handle();
          unsigned char *dest_array_data = dest_handle->get_write_pointer();

          packed_argb_to_uint8_rgba
            (dest_array_data + dest_column->get_start(), 
             dest_array_format->get_stride(),
             array_data + source_column->get_start(), source_array_format->get_stride(),
             num_rows);

        } else {
          // A generic copy.
          if (gobj_cat.is_debug()) {
            gobj_cat.debug()
              << "generic copy " << *dest_column << " from " 
              << *source_column << "\n";
          }
          GeomVertexWriter to(this);
          to.set_column(dest_i, dest_column);
          GeomVertexReader from(source);
          from.set_column(source_i, source_column);

          while (!from.is_at_end()) {
            to.set_data4f(from.get_data4f());
          }
        }
      }
    }
  }

    // Also convert the animation tables as necessary.
  const GeomVertexAnimationSpec &source_animation = source_format->get_animation();
  const GeomVertexAnimationSpec &dest_animation = dest_format->get_animation();
  if (source_animation != dest_animation) {
    if (dest_animation.get_animation_type() == AT_hardware) {
      // Convert Panda-style animation tables to hardware-style
      // animation tables.
      CPT(TransformBlendTable) blend_table = source->get_transform_blend_table();
      if (blend_table != (TransformBlendTable *)NULL) {
        PT(TransformTable) transform_table = new TransformTable;
        TransformMap already_added;

        if (dest_animation.get_indexed_transforms()) {
          // Build an indexed transform array.  This is easier; this
          // means we can put the blends in any order.
          GeomVertexWriter weight(this, InternalName::get_transform_weight());
          GeomVertexWriter index(this, InternalName::get_transform_index());
          GeomVertexReader from(source, InternalName::get_transform_blend());
        
          while (!from.is_at_end()) {
            const TransformBlend &blend = blend_table->get_blend(from.get_data1i());
            LVecBase4f weights = LVecBase4f::zero();
            int indices[4] = {0, 0, 0, 0};
            nassertv(blend.get_num_transforms() <= 4);
            
            for (int i = 0; i < blend.get_num_transforms(); i++) {
              weights[i] = blend.get_weight(i);
              indices[i] = add_transform(transform_table, blend.get_transform(i),
                                         already_added);
            }
            if (weight.has_column()) {
              weight.set_data4f(weights);
            }
            index.set_data4i(indices);
          }
        } else {
          // Build a nonindexed transform array.  This means we have to
          // use the same n transforms, in the same order, for each vertex.
          GeomVertexWriter weight(this, InternalName::get_transform_weight());
          GeomVertexReader from(source, InternalName::get_transform_blend());
        
          while (!from.is_at_end()) {
            const TransformBlend &blend = blend_table->get_blend(from.get_data1i());
            LVecBase4f weights = LVecBase4f::zero();
            
            for (int i = 0; i < blend.get_num_transforms(); i++) {
              int index = add_transform(transform_table, blend.get_transform(i),
                                        already_added);
              nassertv(index <= 4);
              weights[index] = blend.get_weight(i);
            }
            if (weight.has_column()) {
              weight.set_data4f(weights);
            }
          }
        }
        
        clear_transform_blend_table();
        set_transform_table(TransformTable::register_table(transform_table));
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::copy_row_from
//       Access: Published
//  Description: Copies a single row of the data from the other array
//               into the indicated row of this array.  In this case,
//               the source format must exactly match the destination
//               format.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
copy_row_from(int dest_row, const GeomVertexData *source, 
              int source_row, Thread *current_thread) {
  const GeomVertexFormat *source_format = source->get_format();
  const GeomVertexFormat *dest_format = get_format();
  nassertv(source_format == dest_format);
  nassertv(source_row >= 0 && source_row < source->get_num_rows());

  if (dest_row >= get_num_rows()) {
    // Implicitly add enough rows to get to the indicated row.
    set_num_rows(dest_row + 1);
  }

  int num_arrays = source_format->get_num_arrays();

  for (int i = 0; i < num_arrays; ++i) {
    PT(GeomVertexArrayData) dest_array_obj = modify_array(i);
    PT(GeomVertexArrayDataHandle) dest_handle = dest_array_obj->modify_handle();
    unsigned char *dest_array_data = dest_handle->get_write_pointer();

    CPT(GeomVertexArrayData) source_array_obj = source->get_array(i);
    CPT(GeomVertexArrayDataHandle) source_array_handle = source_array_obj->get_handle();
    const unsigned char *source_array_data = source_array_handle->get_read_pointer(true);

    const GeomVertexArrayFormat *array_format = source_format->get_array(i);
    int stride = array_format->get_stride();

    memcpy(dest_array_data + stride * dest_row,
           source_array_data + stride * source_row,
           stride);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::convert_to
//       Access: Published
//  Description: Returns a new GeomVertexData that represents the same
//               contents as this one, with all data types matched up
//               name-by-name to the indicated new format.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
convert_to(const GeomVertexFormat *new_format) const {
  Thread *current_thread = Thread::get_current_thread();

  if (new_format == get_format()) {
    // Trivial case: no change is needed.
    return this;
  }

  // Look up the new format in our cache--maybe we've recently applied
  // it.
  PT(CacheEntry) entry;

  CacheKey key(new_format);

  _cache_lock.lock();
  Cache::const_iterator ci = _cache.find(&key);
  if (ci == _cache.end()) {
    _cache_lock.release();

  } else {
    entry = (*ci).second;
    _cache_lock.release();
    nassertr(entry->_source == this, NULL);

    // Here's an element in the cache for this computation.  Record a
    // cache hit, so this element will stay in the cache a while
    // longer.
    entry->refresh(current_thread);

    CDCacheReader cdata(entry->_cycler);
    if (cdata->_result != (GeomVertexData *)NULL) {
      return cdata->_result;
    }

    // The cache entry is stale, but we'll recompute it below.  Note
    // that there's a small race condition here; another thread might
    // recompute the cache at the same time.  No big deal, since it'll
    // compute the same result.
  }

  // Okay, convert the data to the new format.
  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "Converting " << get_num_rows() << " rows from " << *get_format()
      << " to " << *new_format << "\n";
  }
  PStatTimer timer(_convert_pcollector);

  PT(GeomVertexData) new_data = 
    new GeomVertexData(get_name(), new_format, get_usage_hint());
  new_data->set_transform_blend_table(get_transform_blend_table());
  new_data->set_slider_table(get_slider_table());

  new_data->copy_from(this, false);

  // Record the new result in the cache.
  if (entry == (CacheEntry *)NULL) {
    // Create a new entry for the result.
    entry = new CacheEntry((GeomVertexData *)this, new_format);
    {
      MutexHolder holder(_cache_lock);
      bool inserted = ((GeomVertexData *)this)->_cache.insert(Cache::value_type(&entry->_key, entry)).second;
      if (!inserted) {
        // Some other thread must have beat us to the punch.  Never
        // mind.
        return new_data;
      }
    }
    
    // And tell the cache manager about the new entry.  (It might
    // immediately request a delete from the cache of the thing we
    // just added.)
    entry->record(current_thread);
  }

  // Finally, store the cached result on the entry.
  CDCacheWriter cdata(entry->_cycler, true, current_thread);
  cdata->_result = new_data;

  return new_data;
}


////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::scale_color
//       Access: Published
//  Description: Returns a new GeomVertexData object with the color
//               table modified in-place to apply the indicated scale.
//
//               If the vertex data does not include a color column, a
//               new one will not be added.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
scale_color(const LVecBase4f &color_scale) const {
  const GeomVertexColumn *old_column = 
    get_format()->get_column(InternalName::get_color());
  if (old_column == (GeomVertexColumn *)NULL) {
    return this;
  }

  PT(GeomVertexData) new_data = new GeomVertexData(*this);
  GeomVertexRewriter data(new_data, InternalName::get_color());
  while (!data.is_at_end()) {
    Colorf color = data.get_data4f();
    data.set_data4f(color[0] * color_scale[0],
                    color[1] * color_scale[1],
                    color[2] * color_scale[2],
                    color[3] * color_scale[3]);
  }

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::scale_color
//       Access: Published
//  Description: Returns a new GeomVertexData object with the color
//               table replaced with a new color table that has been
//               scaled by the indicated value.  The new color table
//               will be added as a new array; if the old color table
//               was interleaved with a previous array, the previous
//               array will not be repacked.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
scale_color(const LVecBase4f &color_scale, int num_components,
            GeomVertexData::NumericType numeric_type,
            GeomVertexData::Contents contents) const {
  int old_color_array = get_format()->get_array_with(InternalName::get_color());
  if (old_color_array == -1) {
    // Oops, no color anyway.
    return set_color(color_scale, num_components, numeric_type, contents);
  }

  int num_rows = get_num_rows();

  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "Scaling color for " << num_rows << " vertices by "
      << color_scale << ".\n";
  }
  PStatTimer timer(_scale_color_pcollector);

  PT(GeomVertexData) new_data = replace_column
    (InternalName::get_color(), num_components, numeric_type, contents);

  // Now go through and apply the scale, copying it to the new data.
  GeomVertexWriter to(new_data, InternalName::get_color());
  GeomVertexReader from(this, InternalName::get_color());

  for (int i = 0; i < num_rows; i++) {
    Colorf color = from.get_data4f();
    to.set_data4f(color[0] * color_scale[0],
                  color[1] * color_scale[1],
                  color[2] * color_scale[2],
                  color[3] * color_scale[3]);
  }

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_color
//       Access: Published
//  Description: Returns a new GeomVertexData object with the color
//               data modified in-place with the new value.
//
//               If the vertex data does not include a color column, a
//               new one will not be added.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
set_color(const Colorf &color) const {
  const GeomVertexColumn *old_column = 
    get_format()->get_column(InternalName::get_color());
  if (old_column == (GeomVertexColumn *)NULL) {
    return this;
  }

  PT(GeomVertexData) new_data = new GeomVertexData(*this);
  GeomVertexWriter to(new_data, InternalName::get_color());
  while (!to.is_at_end()) {
    to.set_data4f(color);
  }

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::set_color
//       Access: Published
//  Description: Returns a new GeomVertexData object with the color
//               table replaced with a new color table for which each
//               vertex has the indicated value.  The new color table
//               will be added as a new array; if the old color table
//               was interleaved with a previous array, the previous
//               array will not be repacked.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
set_color(const Colorf &color, int num_components,
          GeomVertexData::NumericType numeric_type,
          GeomVertexData::Contents contents) const {
  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "Setting color for " << get_num_rows() << " vertices to "
      << color << ".\n";
  }
  PStatTimer timer(_set_color_pcollector);

  PT(GeomVertexData) new_data = replace_column
    (InternalName::get_color(), num_components, numeric_type, contents);

  // Now go through and set the new color value.
  GeomVertexWriter to(new_data, InternalName::get_color());
  while (!to.is_at_end()) {
    to.set_data4f(color);
  }

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::reverse_normals
//       Access: Published
//  Description: Returns a new GeomVertexData object with the normal
//               data modified in-place, so that each lighting normal
//               is now facing in the opposite direction.
//
//               If the vertex data does not include a normal column,
//               this returns the original GeomVertexData object,
//               unchanged.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
reverse_normals() const {
  const GeomVertexColumn *old_column = 
    get_format()->get_column(InternalName::get_normal());
  if (old_column == (GeomVertexColumn *)NULL) {
    return this;
  }

  PT(GeomVertexData) new_data = new GeomVertexData(*this);
  GeomVertexRewriter to(new_data, InternalName::get_normal());
  while (!to.is_at_end()) {
    to.set_data3f(-to.get_data3f());
  }

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::animate_vertices
//       Access: Published
//  Description: Returns a GeomVertexData that represents the results
//               of computing the vertex animation on the CPU for this
//               GeomVertexData.
//
//               If there is no CPU-defined vertex animation on this
//               object, this just returns the original object.
//
//               If there is vertex animation, but the VertexTransform
//               values have not changed since last time, this may
//               return the same pointer it returned previously.  Even
//               if the VertexTransform values have changed, it may
//               still return the same pointer, but with its contents
//               modified (this is preferred, since it allows the
//               graphics backend to update vertex buffers optimally).
//
//               If force is false, this method may return immediately
//               with stale data, if the vertex data is not completely
//               resident.  If force is true, this method will never
//               return stale data, but may block until the data is
//               available.
////////////////////////////////////////////////////////////////////
CPT(GeomVertexData) GeomVertexData::
animate_vertices(bool force, Thread *current_thread) const {
  CDLockedReader cdata(_cycler, current_thread);

  if (cdata->_format->get_animation().get_animation_type() != AT_panda) {
    return this;
  }

  UpdateSeq modified;
  if (!cdata->_transform_blend_table.is_null()) {
    if (cdata->_slider_table != (SliderTable *)NULL) {
      modified = 
        max(cdata->_transform_blend_table.get_read_pointer()->get_modified(current_thread),
            cdata->_slider_table->get_modified(current_thread));
    } else {
      modified = cdata->_transform_blend_table.get_read_pointer()->get_modified(current_thread);
    }

  } else if (cdata->_slider_table != (SliderTable *)NULL) {
    modified = cdata->_slider_table->get_modified(current_thread);

  } else {
    // No transform blend table or slider table--ergo, no vertex
    // animation.
    return this;
  }

  if (cdata->_animated_vertices_modified == modified &&
      cdata->_animated_vertices != (GeomVertexData *)NULL) {
    // No changes.
    return cdata->_animated_vertices;
  }

  if (!force && !request_resident()) {
    // The vertex data isn't resident.  Return the best information
    // we've got.
    if (cdata->_animated_vertices != (GeomVertexData *)NULL) {
      return cdata->_animated_vertices;
    }
    return this;
  }

  CDWriter cdataw(((GeomVertexData *)this)->_cycler, cdata, false);
  cdataw->_animated_vertices_modified = modified;
  ((GeomVertexData *)this)->update_animated_vertices(cdataw, current_thread);

  return cdataw->_animated_vertices;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::bytewise_copy
//       Access: Private, Static
//  Description: Quickly copies data without the need to convert it.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
bytewise_copy(unsigned char *to, int to_stride,
              const unsigned char *from, int from_stride,
              const GeomVertexColumn *from_type,
              int num_records) {
  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "bytewise_copy(" << (void *)to << ", " << to_stride
      << ", " << (const void *)from << ", " << from_stride
      << ", " << *from_type << ", " << num_records << ")\n";
  }
  if (to_stride == from_type->get_total_bytes() && 
      from_stride == from_type->get_total_bytes()) {
    // Fantastic!  It's just a linear array of this one data type.
    // Copy the whole thing all at once.
    memcpy(to, from, num_records * from_type->get_total_bytes());

  } else {
    // Ok, it's interleaved in with other data.  Copy them one record
    // at a time.
    while (num_records > 0) {
      memcpy(to, from, from_type->get_total_bytes());
      to += to_stride;
      from += from_stride;
      num_records--;
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::replace_column
//       Access: Published
//  Description: Returns a new GeomVertexData object, suitable for
//               modification, with the indicated data type replaced
//               with a new table filled with undefined values.  The
//               new table will be added as a new array; if the old
//               table was interleaved with a previous array, the
//               previous array will not be repacked.
//
//               If num_components is 0, the indicated name is simply
//               removed from the type, without replacing it with
//               anything else.
////////////////////////////////////////////////////////////////////
PT(GeomVertexData) GeomVertexData::
replace_column(InternalName *name, int num_components,
               GeomVertexData::NumericType numeric_type,
               GeomVertexData::Contents contents) const {
  CDReader cdata(_cycler);
  PT(GeomVertexFormat) new_format = new GeomVertexFormat(*cdata->_format);

  // Remove the old description of the type from the format.
  bool removed_type_array = false;
  int old_type_array = cdata->_format->get_array_with(name);
  if (old_type_array != -1) {
    GeomVertexArrayFormat *array_format = new_format->modify_array(old_type_array);
    if (array_format->get_num_columns() == 1) {
      // Actually, this array didn't have any other data types, so
      // just drop the whole array.
      new_format->remove_array(old_type_array);
      removed_type_array = true;
      
    } else {
      // Remove the description for the type, but don't bother to
      // repack the array.
      array_format->remove_column(name);
    }
  }
    
  // Now define a new array to contain just the type.
  int new_type_array = -1;
  if (num_components != 0) {
    PT(GeomVertexArrayFormat) type_array_format = 
      new GeomVertexArrayFormat(name, num_components, numeric_type, contents);
    new_type_array = new_format->add_array(type_array_format);
  }

  CPT(GeomVertexFormat) format = 
    GeomVertexFormat::register_format(new_format);

  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "Replacing data type " << *name << "; converting "
      << get_num_rows() << " rows from " 
      << *cdata->_format << " to " << *format << "\n";
  }
  
  PT(GeomVertexData) new_data = new GeomVertexData(*this, format);

  int j = 0;
  int num_arrays = get_num_arrays();
  for (int i = 0; i < num_arrays; ++i) {
    if (i == old_type_array) {
      if (!removed_type_array) {
        // Pointer-copy the original array that includes the type
        // (since it also includes other data).
        new_data->set_array(j, get_array(i));
        ++j;
      }

    } else {
      // Just pointer-copy any arrays other than type.
      new_data->set_array(j, get_array(i));
      ++j;
    }
  }

  if (new_type_array != -1) {
    nassertr(j == new_type_array, new_data);

    // For the new type array, we set up a temporary array that has
    // room for the right number of rows.
    PT(GeomVertexArrayData) new_array = new GeomVertexArrayData
      (format->get_array(j), get_usage_hint());
    new_array->set_num_rows(get_num_rows());
    new_data->set_array(j, new_array);
  }

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::output
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexData::
output(ostream &out) const {
  if (!get_name().empty()) {
    out << get_name() << " ";
  }
  out << get_num_rows() << " rows: " << *get_format();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::write
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexData::
write(ostream &out, int indent_level) const {
  if (!get_name().empty()) {
    indent(out, indent_level) << get_name() << "\n";
  }
  get_format()->write_with_data(out, indent_level + 2, this);
  CPT(TransformBlendTable) table = get_transform_blend_table();
  if (table != (TransformBlendTable *)NULL) {
    indent(out, indent_level)
      << "Transform blend table:\n";
    table->write(out, indent_level + 2);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::clear_cache
//       Access: Published
//  Description: Removes all of the previously-cached results of
//               convert_to().
//
//               This blows away the entire cache, upstream and
//               downstream the pipeline.  Use clear_cache_stage()
//               instead if you only want to blow away the cache at
//               the current stage and upstream.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
clear_cache() {
  MutexHolder holder(_cache_lock);
  for (Cache::iterator ci = _cache.begin();
       ci != _cache.end();
       ++ci) {
    CacheEntry *entry = (*ci).second;
    entry->erase();
  }
  _cache.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::clear_cache_stage
//       Access: Published
//  Description: Removes all of the previously-cached results of
//               convert_to(), at the current pipeline stage and
//               upstream.  Does not affect the downstream cache.
//
//               Don't call this in a downstream thread unless you
//               don't mind it blowing away other changes you might
//               have recently made in an upstream thread.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
clear_cache_stage() {
  MutexHolder holder(_cache_lock);
  for (Cache::iterator ci = _cache.begin();
       ci != _cache.end();
       ++ci) {
    CacheEntry *entry = (*ci).second;
    CDCacheWriter cdata(entry->_cycler);
    cdata->_result = NULL;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::packed_argb_to_uint8_rgba
//       Access: Private, Static
//  Description: Quickly converts DirectX-style color to OpenGL-style
//               color.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
packed_argb_to_uint8_rgba(unsigned char *to, int to_stride,
                          const unsigned char *from, int from_stride,
                          int num_records) {
  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "packed_argb_to_uint8_rgba(" << (void *)to << ", " << to_stride
      << ", " << (const void *)from << ", " << from_stride
      << ", " << num_records << ")\n";
  }

  while (num_records > 0) {
    PN_uint32 dword = *(const PN_uint32 *)from;
    to[0] = unpack_abcd_b(dword);
    to[1] = unpack_abcd_c(dword);
    to[2] = unpack_abcd_d(dword);
    to[3] = unpack_abcd_a(dword);

    to += to_stride;
    from += from_stride;
    num_records--;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::uint8_rgba_to_packed_argb
//       Access: Private, Static
//  Description: Quickly converts OpenGL-style color to DirectX-style
//               color.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
uint8_rgba_to_packed_argb(unsigned char *to, int to_stride,
                          const unsigned char *from, int from_stride,
                          int num_records) {
  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "uint8_rgba_to_packed_argb(" << (void *)to << ", " << to_stride
      << ", " << (const void *)from << ", " << from_stride
      << ", " << num_records << ")\n";
  }

  while (num_records > 0) {
    *(PN_uint32 *)to = pack_abcd(from[3], from[0], from[1], from[2]);

    to += to_stride;
    from += from_stride;
    num_records--;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::update_animated_vertices
//       Access: Private
//  Description: Recomputes the results of computing the vertex
//               animation on the CPU, and applies them to the
//               existing animated_vertices object.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
update_animated_vertices(GeomVertexData::CData *cdata, Thread *current_thread) {
  int num_rows = get_num_rows();

  if (gobj_cat.is_debug()) {
    gobj_cat.debug()
      << "Animating " << num_rows << " vertices for " << get_name()
      << "\n";
  }

  PStatTimer timer(_char_pcollector, current_thread);

  const GeomVertexFormat *orig_format = cdata->_format;

  if (cdata->_animated_vertices == (GeomVertexData *)NULL) {
    CPT(GeomVertexFormat) new_format = orig_format->get_post_animated_format();
    cdata->_animated_vertices = 
      new GeomVertexData(get_name(), new_format,
                         min(get_usage_hint(), UH_dynamic));
  }
  PT(GeomVertexData) new_data = cdata->_animated_vertices;

  // We have to make a complete copy of the data first so we can
  // modify it.  If we were clever, we could maybe just figure out the
  // subset of the data that might have changed since last frame, but
  // that's too much trouble (and isn't obviously faster than just
  // copying the whole thing).
  new_data->copy_from(this, true);

  // First, apply all of the morphs.
  CPT(SliderTable) slider_table = cdata->_slider_table;
  if (slider_table != (SliderTable *)NULL) {
    PStatTimer timer2(_morphs_pcollector);
    int num_morphs = orig_format->get_num_morphs();
    for (int mi = 0; mi < num_morphs; mi++) {
      CPT(InternalName) slider_name = orig_format->get_morph_slider(mi);

      const SparseArray &sliders = slider_table->find_sliders(slider_name);
      if (!sliders.is_zero()) {
        nassertv(!sliders.is_inverse());
        int num_slider_subranges = sliders.get_num_subranges();
        for (int sni = 0; sni < num_slider_subranges; ++sni) {
          int slider_begin = sliders.get_subrange_begin(sni);
          int slider_end = sliders.get_subrange_end(sni);
          for (int sn = slider_begin; sn < slider_end; ++sn) {
            const VertexSlider *slider = slider_table->get_slider(sn);
            const SparseArray &rows = slider_table->get_slider_rows(sn);
            nassertv(!rows.is_inverse());

            float slider_value = slider->get_slider();
            if (slider_value != 0.0f) {
              CPT(InternalName) base_name = orig_format->get_morph_base(mi);
              CPT(InternalName) delta_name = orig_format->get_morph_delta(mi);

              GeomVertexRewriter data(new_data, base_name);
              GeomVertexReader delta(this, delta_name);
              int num_subranges = rows.get_num_subranges();

              if (data.get_column()->get_num_values() == 4) {
                if (data.get_column()->has_homogeneous_coord()) {
                  // Scale the delta by the homogeneous coordinate.
                  for (int i = 0; i < num_subranges; ++i) {
                    int begin = rows.get_subrange_begin(i);
                    int end = rows.get_subrange_end(i);
                    data.set_row(begin);
                    delta.set_row(begin);
                    for (int j = begin; j < end; ++j) {
                      LPoint4f vertex = data.get_data4f();
                      LPoint3f d = delta.get_data3f();
                      d *= slider_value * vertex[3];
                      data.set_data4f(vertex[0] + d[0],
                                      vertex[1] + d[1],
                                      vertex[2] + d[2],
                                      vertex[3]);
                    }
                  }
                } else {
                  // Just apply the four-component delta.
                  for (int i = 0; i < num_subranges; ++i) {
                    int begin = rows.get_subrange_begin(i);
                    int end = rows.get_subrange_end(i);
                    data.set_row(begin);
                    delta.set_row(begin);
                    for (int j = begin; j < end; ++j) {
                      const LPoint4f &vertex = data.get_data4f();
                      LPoint4f d = delta.get_data4f();
                      data.set_data4f(vertex + d * slider_value);
                    }
                  }
                }
              } else {
                // 3-component or smaller values; don't worry about a
                // homogeneous coordinate.
                for (int i = 0; i < num_subranges; ++i) {
                  int begin = rows.get_subrange_begin(i);
                  int end = rows.get_subrange_end(i);
                  data.set_row(begin);
                  delta.set_row(begin);
                  for (int j = begin; j < end; ++j) {
                    const LPoint3f &vertex = data.get_data3f();
                    LPoint3f d = delta.get_data3f();
                    data.set_data3f(vertex + d * slider_value);
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  // Then apply the transforms.
  CPT(TransformBlendTable) tb_table = cdata->_transform_blend_table.get_read_pointer();
  if (tb_table != (TransformBlendTable *)NULL) {
    PStatTimer timer3(_skinning_pcollector);

    // Recompute all the blends up front, so we don't have to test
    // each one for staleness at each vertex.
    int num_blends = tb_table->get_num_blends();
    int bi;
    for (bi = 0; bi < num_blends; bi++) {
      tb_table->get_blend(bi).update_blend(current_thread);
    }

    // Now go through and apply the transforms.
    GeomVertexReader blendi(this, InternalName::get_transform_blend());
    if (!blendi.has_column()) {
      gobj_cat.warning()
        << "Vertex data " << get_name()
        << " has a transform_blend_table, but no transform_blend data.\n";
      return;
    }

    const SparseArray &rows = tb_table->get_rows();
    int num_subranges = rows.get_num_subranges();

    int ci;
    for (ci = 0; ci < orig_format->get_num_points(); ci++) {
      GeomVertexRewriter data(new_data, orig_format->get_point(ci));
      
      if (data.get_column()->get_num_values() == 4) {
        for (int i = 0; i < num_subranges; ++i) {
          int begin = rows.get_subrange_begin(i);
          int end = rows.get_subrange_end(i);
          data.set_row(begin);
          blendi.set_row(begin);
          for (int j = begin; j < end; ++j) {
            LPoint4f vertex = data.get_data4f();
            int bi = blendi.get_data1i();
            tb_table->get_blend(bi).transform_point(vertex, current_thread);
            data.set_data4f(vertex);
          }
        }
      } else {
        for (int i = 0; i < num_subranges; ++i) {
          int begin = rows.get_subrange_begin(i);
          int end = rows.get_subrange_end(i);
          data.set_row(begin);
          blendi.set_row(begin);
          for (int j = begin; j < end; ++j) {
            LPoint3f vertex = data.get_data3f();
            int bi = blendi.get_data1i();
            tb_table->get_blend(bi).transform_point(vertex, current_thread);
            data.set_data3f(vertex);
          }
        }
      }
    }
    for (ci = 0; ci < orig_format->get_num_vectors(); ci++) {
      GeomVertexRewriter data(new_data, orig_format->get_vector(ci));
      for (int i = 0; i < num_subranges; ++i) {
        int begin = rows.get_subrange_begin(i);
        int end = rows.get_subrange_end(i);
        data.set_row(begin);
        blendi.set_row(begin);
        for (int j = begin; j < end; ++j) {
          LVector3f vertex = data.get_data3f();
          int bi = blendi.get_data1i();
          tb_table->get_blend(bi).transform_vector(vertex, current_thread);
          data.set_data3f(vertex);
        }
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::register_with_read_factory
//       Access: Public, Static
//  Description: Tells the BamReader how to create objects of type
//               GeomVertexData.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
register_with_read_factory() {
  BamReader::get_factory()->register_factory(get_class_type(), make_from_bam);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::write_datagram
//       Access: Public, Virtual
//  Description: Writes the contents of this object to the datagram
//               for shipping out to a Bam file.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
write_datagram(BamWriter *manager, Datagram &dg) {
  CopyOnWriteObject::write_datagram(manager, dg);

  dg.add_string(_name);
  manager->write_cdata(dg, _cycler);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::make_from_bam
//       Access: Protected, Static
//  Description: This function is called by the BamReader's factory
//               when a new object of type GeomVertexData is encountered
//               in the Bam file.  It should create the GeomVertexData
//               and extract its information from the file.
////////////////////////////////////////////////////////////////////
TypedWritable *GeomVertexData::
make_from_bam(const FactoryParams &params) {
  GeomVertexData *object = new GeomVertexData;
  DatagramIterator scan;
  BamReader *manager;

  parse_params(params, scan, manager);
  object->fillin(scan, manager);
  manager->register_finalize(object);

  return object;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::complete_pointers
//       Access: Public, Virtual
//  Description: Receives an array of pointers, one for each time
//               manager->read_pointer() was called in fillin().
//               Returns the number of pointers processed.
////////////////////////////////////////////////////////////////////
int GeomVertexData::
complete_pointers(TypedWritable **p_list, BamReader *manager) {
  int pi = CopyOnWriteObject::complete_pointers(p_list, manager);
  return pi;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::require_fully_complete
//       Access: Public, Virtual
//  Description: Some objects require all of their nested pointers to
//               have been completed before the objects themselves can
//               be completed.  If this is the case, override this
//               method to return true, and be careful with circular
//               references (which would make the object unreadable
//               from a bam file).
////////////////////////////////////////////////////////////////////
bool GeomVertexData::
require_fully_complete() const {
  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::finalize
//       Access: Public, Virtual
//  Description: Called by the BamReader to perform any final actions
//               needed for setting up the object after all objects
//               have been read and all pointers have been completed.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
finalize(BamReader *manager) {
  // NOTE: This method may be called more than once, because the
  // Geom::finalize() will call it explicitly.  We have to be prepared
  // to accept multiple finalize() calls.

  // Now we need to register the format that we have read from the bam
  // file (since it doesn't come out of the bam file automatically
  // registered).  This may change the format's pointer, which we
  // should then update our own data to reflect.  But since this may
  // cause the unregistered object to destruct, we have to also tell
  // the BamReader to return the new object from now on.

  // This extends to the nested array datas, as well as the transform
  // table and slider tables, as well.

  CDWriter cdata(_cycler, true);

  for (size_t i = 0; i < cdata->_arrays.size(); ++i) {
    CPT(GeomVertexFormat) new_format = 
      GeomVertexFormat::register_format(cdata->_format);
    manager->change_pointer(cdata->_format, new_format);
    cdata->_format = new_format;

    CPT(GeomVertexArrayFormat) new_array_format = new_format->get_array(i);
    PT(GeomVertexArrayData) array_obj = cdata->_arrays[i].get_unsafe_pointer();
    nassertv(new_array_format->is_data_subset_of(*array_obj->_array_format));

    manager->change_pointer(array_obj->_array_format, new_array_format);
    array_obj->_array_format = new_array_format;
  }

  if (cdata->_transform_table != (TransformTable *)NULL) {
    CPT(TransformTable) new_transform_table = 
      TransformTable::register_table(cdata->_transform_table);
    manager->change_pointer(cdata->_transform_table, new_transform_table);
    cdata->_transform_table = new_transform_table;
  }

  if (cdata->_slider_table != (SliderTable *)NULL) {
    CPT(SliderTable) new_slider_table = 
      SliderTable::register_table(cdata->_slider_table);
    manager->change_pointer(cdata->_slider_table, new_slider_table);
    cdata->_slider_table = new_slider_table;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::fillin
//       Access: Protected
//  Description: This internal function is called by make_from_bam to
//               read in all of the relevant data from the BamFile for
//               the new GeomVertexData.
////////////////////////////////////////////////////////////////////
void GeomVertexData::
fillin(DatagramIterator &scan, BamReader *manager) {
  CopyOnWriteObject::fillin(scan, manager);

  set_name(scan.get_string());
  manager->read_cdata(scan, _cycler);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CDataCache::make_copy
//       Access: Public, Virtual
//  Description:
////////////////////////////////////////////////////////////////////
CycleData *GeomVertexData::CDataCache::
make_copy() const {
  return new CDataCache(*this);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CacheEntry::evict_callback
//       Access: Public, Virtual
//  Description: Called when the entry is evicted from the cache, this
//               should clean up the owning object appropriately.
////////////////////////////////////////////////////////////////////
void GeomVertexData::CacheEntry::
evict_callback() {
  MutexHolder holder(_source->_cache_lock);
  Cache::iterator ci = _source->_cache.find(&_key);
  nassertv(ci != _source->_cache.end());
  nassertv((*ci).second == this);
  _source->_cache.erase(ci);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CacheEntry::output
//       Access: Public, Virtual
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexData::CacheEntry::
output(ostream &out) const {
  out << "vertex data " << (void *)_source << " to " 
      << *_key._modifier;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CData::make_copy
//       Access: Public, Virtual
//  Description:
////////////////////////////////////////////////////////////////////
CycleData *GeomVertexData::CData::
make_copy() const {
  return new CData(*this);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CData::write_datagram
//       Access: Public, Virtual
//  Description: Writes the contents of this object to the datagram
//               for shipping out to a Bam file.
////////////////////////////////////////////////////////////////////
void GeomVertexData::CData::
write_datagram(BamWriter *manager, Datagram &dg) const {
  manager->write_pointer(dg, _format);
  dg.add_uint8(_usage_hint);

  dg.add_uint16(_arrays.size());
  Arrays::const_iterator ai;
  for (ai = _arrays.begin(); ai != _arrays.end(); ++ai) {
    manager->write_pointer(dg, (*ai).get_read_pointer());
  }

  manager->write_pointer(dg, _transform_table);
  manager->write_pointer(dg, _transform_blend_table.get_read_pointer());
  manager->write_pointer(dg, _slider_table);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CData::complete_pointers
//       Access: Public, Virtual
//  Description: Receives an array of pointers, one for each time
//               manager->read_pointer() was called in fillin().
//               Returns the number of pointers processed.
////////////////////////////////////////////////////////////////////
int GeomVertexData::CData::
complete_pointers(TypedWritable **p_list, BamReader *manager) {
  int pi = CycleData::complete_pointers(p_list, manager);

  _format = DCAST(GeomVertexFormat, p_list[pi++]);

  Arrays::iterator ai;
  for (ai = _arrays.begin(); ai != _arrays.end(); ++ai) {
    (*ai) = DCAST(GeomVertexArrayData, p_list[pi++]);    
  }

  _transform_table = DCAST(TransformTable, p_list[pi++]);
  _transform_blend_table = DCAST(TransformBlendTable, p_list[pi++]);
  _slider_table = DCAST(SliderTable, p_list[pi++]);

  _modified = Geom::get_next_modified();

  if (!_arrays.empty() && manager->get_file_minor_ver() < 7) {
    // Bam files prior to 6.7 did not store a SparseArray in the
    // SliderTable or TransformBlendTable entries.  We need to make up
    // a SparseArray for each of them that reflects the complete
    // number of rows in the data.
    SparseArray all_rows;
    CPT(GeomVertexArrayData) adata = _arrays[0].get_read_pointer();
    all_rows.set_range(0, adata->get_num_rows());

    if (_slider_table != (SliderTable *)NULL) {
      int num_sliders = _slider_table->get_num_sliders();
      for (int i = 0; i < num_sliders; ++i) {
        ((SliderTable *)_slider_table.p())->set_slider_rows(i, all_rows);
      }
    }
    if (!_transform_blend_table.is_null()) {
      _transform_blend_table.get_unsafe_pointer()->set_rows(all_rows);
    }
  }

  return pi;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexData::CData::fillin
//       Access: Public, Virtual
//  Description: This internal function is called by make_from_bam to
//               read in all of the relevant data from the BamFile for
//               the new GeomVertexData.
////////////////////////////////////////////////////////////////////
void GeomVertexData::CData::
fillin(DatagramIterator &scan, BamReader *manager) {
  manager->read_pointer(scan);
  _usage_hint = (UsageHint)scan.get_uint8();

  size_t num_arrays = scan.get_uint16();
  _arrays.reserve(num_arrays);
  for (size_t i = 0; i < num_arrays; ++i) {
    manager->read_pointer(scan);
    _arrays.push_back(NULL);
  }

  manager->read_pointer(scan);
  manager->read_pointer(scan);
  manager->read_pointer(scan);
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineBase::get_num_bytes
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
int GeomVertexDataPipelineBase::
get_num_bytes() const {
  int num_bytes = sizeof(GeomVertexData);

  GeomVertexData::Arrays::const_iterator ai;
  for (ai = _cdata->_arrays.begin(); ai != _cdata->_arrays.end(); ++ai) {
    num_bytes += (*ai).get_read_pointer()->get_data_size_bytes();
  }

  return num_bytes;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::get_num_rows
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
int GeomVertexDataPipelineReader::
get_num_rows() const {
  nassertr(_cdata->_format->get_num_arrays() == (int)_cdata->_arrays.size(), 0);
  nassertr(_got_array_readers, 0);

  if (_cdata->_format->get_num_arrays() == 0) {
    // No arrays means no rows.  Weird but legal.
    return 0;
  }

  // Look up the answer on the first array (since any array will do).
  int stride = _cdata->_format->get_array(0)->get_stride();
  return _array_readers[0]->get_data_size_bytes() / stride;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::get_array_info
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
bool GeomVertexDataPipelineReader::
get_array_info(const InternalName *name, 
               const GeomVertexArrayDataHandle *&array_reader,
               int &num_values, 
               GeomVertexDataPipelineReader::NumericType &numeric_type, 
               int &start, int &stride) const {
  nassertr(_got_array_readers, false);
  int array_index;
  const GeomVertexColumn *column;
  if (_cdata->_format->get_array_info(name, array_index, column)) {
    array_reader = _array_readers[array_index];
    num_values = column->get_num_values();
    numeric_type = column->get_numeric_type();
    start = column->get_start();
    stride = _cdata->_format->get_array(array_index)->get_stride();
    return true;
  }
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::get_vertex_info
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
bool GeomVertexDataPipelineReader::
get_vertex_info(const GeomVertexArrayDataHandle *&array_reader,
                int &num_values, 
                GeomVertexDataPipelineReader::NumericType &numeric_type, 
                int &start, int &stride) const {
  nassertr(_got_array_readers, false);
  int array_index = _cdata->_format->get_vertex_array_index();
  if (array_index >= 0) {
    const GeomVertexColumn *column = _cdata->_format->get_vertex_column();

    array_reader = _array_readers[array_index];
    num_values = column->get_num_values();
    numeric_type = column->get_numeric_type();
    start = column->get_start();
    stride = _cdata->_format->get_array(array_index)->get_stride();
    return true;
  }
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::get_normal_info
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
bool GeomVertexDataPipelineReader::
get_normal_info(const GeomVertexArrayDataHandle *&array_reader,
                GeomVertexDataPipelineReader::NumericType &numeric_type, 
                int &start, int &stride) const {
  nassertr(_got_array_readers, false);
  int array_index = _cdata->_format->get_normal_array_index();
  if (array_index >= 0) {
    const GeomVertexColumn *column = _cdata->_format->get_normal_column();
    nassertr(column->get_num_values() == 3, false);

    array_reader = _array_readers[array_index];
    numeric_type = column->get_numeric_type();
    start = column->get_start();
    stride = _cdata->_format->get_array(array_index)->get_stride();
    return true;
  }
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::get_color_info
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
bool GeomVertexDataPipelineReader::
get_color_info(const GeomVertexArrayDataHandle *&array_reader,
               int &num_values, 
               GeomVertexDataPipelineReader::NumericType &numeric_type, 
               int &start, int &stride) const {
  nassertr(_got_array_readers, false);
  int array_index = _cdata->_format->get_color_array_index();
  if (array_index >= 0) {
    const GeomVertexColumn *column = _cdata->_format->get_color_column();

    array_reader = _array_readers[array_index];
    num_values = column->get_num_values();
    numeric_type = column->get_numeric_type();
    start = column->get_start();
    stride = _cdata->_format->get_array(array_index)->get_stride();
    return true;
  }
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::make_array_readers
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexDataPipelineReader::
make_array_readers() {
  nassertv(!_got_array_readers);

  _array_readers.reserve(_cdata->_arrays.size());
  GeomVertexData::Arrays::const_iterator ai;
  for (ai = _cdata->_arrays.begin(); ai != _cdata->_arrays.end(); ++ai) {
    CPT(GeomVertexArrayData) array_obj = (*ai).get_read_pointer();
    _array_readers.push_back(array_obj->get_handle(_current_thread));
  }

  _got_array_readers = true;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineReader::delete_array_readers
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexDataPipelineReader::
delete_array_readers() {
  nassertv(_got_array_readers);

  _array_readers.clear();
  _got_array_readers = false;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::get_num_rows
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
int GeomVertexDataPipelineWriter::
get_num_rows() const {
  nassertr(_cdata->_format->get_num_arrays() == (int)_cdata->_arrays.size(), 0);
  nassertr(_got_array_writers, 0);

  if (_cdata->_format->get_num_arrays() == 0) {
    // No arrays means no rows.  Weird but legal.
    return 0;
  }

  // Look up the answer on the first array (since any array will do).
  int stride = _cdata->_format->get_array(0)->get_stride();
  return _array_writers[0]->get_data_size_bytes() / stride;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::set_num_rows
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
bool GeomVertexDataPipelineWriter::
set_num_rows(int n) {
  nassertr(_got_array_writers, false);
  nassertr(_cdata->_format->get_num_arrays() == (int)_cdata->_arrays.size(), false);

  bool any_changed = false;

  int color_array = -1;
  int orig_color_rows = -1;

  for (size_t i = 0; i < _cdata->_arrays.size(); i++) {
    if (_array_writers[i]->get_num_rows() != n) {
      if (_array_writers[i]->get_object()->has_column(InternalName::get_color())) {
        color_array = i;
        orig_color_rows = _array_writers[i]->get_num_rows();
      }
      _array_writers[i]->set_num_rows(n);
      any_changed = true;
    }
  }

  if (color_array >= 0 && orig_color_rows < n) {
    // We have just added some rows; fill the "color" column with
    // (1, 1, 1, 1), for the programmer's convenience.
    GeomVertexArrayDataHandle *array_writer = _array_writers[color_array];
    const GeomVertexArrayFormat *array_format = array_writer->get_array_format();
    const GeomVertexColumn *column = 
      array_format->get_column(InternalName::get_color());
    int stride = array_format->get_stride();
    unsigned char *start = 
      array_writer->get_write_pointer() + column->get_start();
    unsigned char *stop = start + array_writer->get_data_size_bytes();
    unsigned char *pointer = start + stride * orig_color_rows;
    int num_values = column->get_num_values();

    switch (column->get_numeric_type()) {
    case NT_packed_dcba:
    case NT_packed_dabc:
    case NT_uint8:
    case NT_uint16:
    case NT_uint32:
      while (pointer < stop) {
        memset(pointer, 0xff, column->get_total_bytes());
        pointer += stride;
      }
      break;

    case NT_float32:
      while (pointer < stop) {
        PN_float32 *pi = (PN_float32 *)pointer;
        for (int i = 0; i < num_values; i++) {
          pi[i] = 1.0f;
        }
        pointer += stride;
      }
      break;
    }          
  }

  if (any_changed) {
    _object->clear_cache_stage();
    _cdata->_modified = Geom::get_next_modified();
    _cdata->_animated_vertices.clear();
  }

  return any_changed;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::unclean_set_num_rows
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
bool GeomVertexDataPipelineWriter::
unclean_set_num_rows(int n) {
  nassertr(_got_array_writers, false);
  nassertr(_cdata->_format->get_num_arrays() == (int)_cdata->_arrays.size(), false);

  bool any_changed = false;

  for (size_t i = 0; i < _cdata->_arrays.size(); i++) {
    if (_array_writers[i]->get_num_rows() != n) {
      _array_writers[i]->unclean_set_num_rows(n);
      any_changed = true;
    }
  }

  if (any_changed) {
    _object->clear_cache_stage();
    _cdata->_modified = Geom::get_next_modified();
    _cdata->_animated_vertices.clear();
  }

  return any_changed;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::modify_array
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
PT(GeomVertexArrayData) GeomVertexDataPipelineWriter::
modify_array(int i) {
  nassertr(i >= 0 && i < (int)_cdata->_arrays.size(), NULL);

  PT(GeomVertexArrayData) new_data;
  if (_got_array_writers) {
    new_data = _array_writers[i]->get_object();
  } else {
    new_data = _cdata->_arrays[i].get_write_pointer();
  }

  _object->clear_cache_stage();
  _cdata->_modified = Geom::get_next_modified();
  _cdata->_animated_vertices_modified = UpdateSeq();

  return new_data;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::set_array
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexDataPipelineWriter::
set_array(int i, const GeomVertexArrayData *array) {
  nassertv(i >= 0 && i < (int)_cdata->_arrays.size());
  _cdata->_arrays[i] = (GeomVertexArrayData *)array;
  _object->clear_cache_stage();
  _cdata->_modified = Geom::get_next_modified();
  _cdata->_animated_vertices_modified = UpdateSeq();

  if (_got_array_writers) {
    _array_writers[i] = _cdata->_arrays[i].get_write_pointer()->modify_handle(_current_thread);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::make_array_writers
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexDataPipelineWriter::
make_array_writers() {
  nassertv(!_got_array_writers);

  _array_writers.reserve(_cdata->_arrays.size());
  GeomVertexData::Arrays::iterator ai;
  for (ai = _cdata->_arrays.begin(); ai != _cdata->_arrays.end(); ++ai) {
    PT(GeomVertexArrayData) array_obj = (*ai).get_write_pointer();
    _array_writers.push_back(array_obj->modify_handle(_current_thread));
  }

  _object->clear_cache_stage();
  _cdata->_modified = Geom::get_next_modified();
  _cdata->_animated_vertices_modified = UpdateSeq();

  _got_array_writers = true;
}

////////////////////////////////////////////////////////////////////
//     Function: GeomVertexDataPipelineWriter::delete_array_writers
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void GeomVertexDataPipelineWriter::
delete_array_writers() {
  nassertv(_got_array_writers);

  _array_writers.clear();
  _got_array_writers = false;
}
