diff --git a/include/FluidSCWrapper.hpp b/include/FluidSCWrapper.hpp index ce3f07b..7bf1790 100644 --- a/include/FluidSCWrapper.hpp +++ b/include/FluidSCWrapper.hpp @@ -13,6 +13,7 @@ under the European Union’s Horizon 2020 research and innovation programme #include "SCBufferAdaptor.hpp" #include #include +#include #include #include #include @@ -87,16 +88,20 @@ class RealTime : public SCUnit using ParamSetType = typename Client::ParamSetType; public: + + static index ControlOffset(Unit* unit) { return unit->mSpecialIndex + 1; } + static index ControlSize(Unit* unit) { return static_cast(unit->mNumInputs) - unit->mSpecialIndex - 1; } + static void setup(InterfaceTable* ft, const char* name) { - registerUnit(ft, name); ft->fDefineUnitCmd(name, "latency", doLatency); } static void doLatency(Unit* unit, sc_msg_iter*) { float l[]{ - static_cast(static_cast(unit)->mClient.latency())}; + static_cast(static_cast(unit)->mClient.latency()) + }; auto ft = Wrapper::getInterfaceTable(); std::stringstream ss; @@ -106,55 +111,51 @@ public: } RealTime() - : mControlsIterator{mInBuf + mSpecialIndex + 1, - static_cast(mNumInputs) - mSpecialIndex - 1}, - mParams{Wrapper::Client::getParameterDescriptors()}, - mClient{Wrapper::setParams(static_cast(this), - mParams, mWorld->mVerbosity > 0, mWorld, - mControlsIterator, true)} {} void init() { + + auto& client =static_cast(this)->mClient; assert( - !(mClient.audioChannelsOut() > 0 && mClient.controlChannelsOut() > 0) && + !(client.audioChannelsOut() > 0 && client.controlChannelsOut() > 0) && "Client can't have both audio and control outputs"); // If we don't the number of arguments we expect, the language side code is // probably the wrong version set plugin to no-op, squawk, and bail; - if (mControlsIterator.size() != Client::getParameterDescriptors().count()) + if (static_cast(this)->mControlsIterator.size() != Client::getParameterDescriptors().count()) { mCalcFunc = Wrapper::getInterfaceTable()->fClearUnitOutputs; std::cout << "ERROR: " << Wrapper::getName() << " wrong number of arguments. Expected " << Client::getParameterDescriptors().count() << ", got " - << mControlsIterator.size() + << static_cast(this)->mControlsIterator.size() << ". Your .sc file and binary plugin might be different versions." << std::endl; return; } - mClient.sampleRate(fullSampleRate()); - mInputConnections.reserve(asUnsigned(mClient.audioChannelsIn())); - mOutputConnections.reserve(asUnsigned(mClient.audioChannelsOut())); - mAudioInputs.reserve(asUnsigned(mClient.audioChannelsIn())); + client.sampleRate(fullSampleRate()); + mInputConnections.reserve(asUnsigned(client.audioChannelsIn())); + mOutputConnections.reserve(asUnsigned(client.audioChannelsOut())); + mAudioInputs.reserve(asUnsigned(client.audioChannelsIn())); mOutputs.reserve(asUnsigned( - std::max(mClient.audioChannelsOut(), mClient.controlChannelsOut()))); + std::max(client.audioChannelsOut(), client.controlChannelsOut()))); - for (index i = 0; i < mClient.audioChannelsIn(); ++i) + for (index i = 0; i < client.audioChannelsIn(); ++i) { mInputConnections.emplace_back(isAudioRateIn(static_cast(i))); mAudioInputs.emplace_back(nullptr, 0, 0); } - for (index i = 0; i < mClient.audioChannelsOut(); ++i) + for (index i = 0; i < client.audioChannelsOut(); ++i) { mOutputConnections.emplace_back(true); mOutputs.emplace_back(nullptr, 0, 0); } - for (index i = 0; i < mClient.controlChannelsOut(); ++i) + for (index i = 0; i < client.controlChannelsOut(); ++i) { mOutputs.emplace_back(nullptr, 0, 0); } mCalcFunc = make_calc_function(); @@ -163,49 +164,41 @@ public: void next(int) { - mControlsIterator.reset(mInBuf + mSpecialIndex + + + auto& client = static_cast(this)->mClient; + auto& params = static_cast(this)->mParams; + + static_cast(this)->mControlsIterator.reset(mInBuf + mSpecialIndex + 1); // mClient.audioChannelsIn()); Wrapper::setParams(static_cast(this), - mParams, mWorld->mVerbosity > 0, mWorld, - mControlsIterator); // forward on inputs N + audio inputs as params - mParams.constrainParameterValues(); + params, static_cast(this)->mControlsIterator); // forward on inputs N + audio inputs as params + params.constrainParameterValues(); const Unit* unit = this; - for (index i = 0; i < mClient.audioChannelsIn(); ++i) + for (index i = 0; i < client.audioChannelsIn(); ++i) { if (mInputConnections[asUnsigned(i)]) { mAudioInputs[asUnsigned(i)].reset(IN(i), 0, fullBufferSize()); } } - for (index i = 0; i < mClient.audioChannelsOut(); ++i) + for (index i = 0; i < client.audioChannelsOut(); ++i) { assert(i <= std::numeric_limits::max()); if (mOutputConnections[asUnsigned(i)]) mOutputs[asUnsigned(i)].reset(out(static_cast(i)), 0, fullBufferSize()); } - for (index i = 0; i < mClient.controlChannelsOut(); ++i) + for (index i = 0; i < client.controlChannelsOut(); ++i) { assert(i <= std::numeric_limits::max()); mOutputs[asUnsigned(i)].reset(out(static_cast(i)), 0, 1); } - mClient.process(mAudioInputs, mOutputs, mContext); + client.process(mAudioInputs, mOutputs, mContext); } - - ParamSetType& params() - { - return mParams; - } - private: std::vector mInputConnections; std::vector mOutputConnections; std::vector mAudioInputs; std::vector mOutputs; - FloatControlsIter mControlsIterator; FluidContext mContext; - -protected: - ParamSetType mParams; - Client mClient; }; //////////////////////////////////////////////////////////////////////////////// @@ -220,9 +213,12 @@ class NonRealTime : public SCUnit using ParamSetType = typename Client::ParamSetType; public: + + static index ControlOffset(Unit*) { return 0; } + static index ControlSize(Unit* unit) { return index(unit->mNumInputs) - unit->mSpecialIndex - 2; } + static void setup(InterfaceTable* ft, const char* name) { - registerUnit(ft, name); ft->fDefineUnitCmd(name, "cancel", doCancel); ft->fDefineUnitCmd( name, "queue_enabled", [](struct Unit* unit, struct sc_msg_iter* args) { @@ -253,18 +249,14 @@ public: } /// Penultimate input is the doneAction, final is blocking mode. Neither are - /// params, so we skip them in the controlsIterator + /// params, so we skip them in the controlsIterator. We may also have an ID for Model objects NonRealTime() - : mControlsIterator{mInBuf, index(mNumInputs) - mSpecialIndex - 2}, - mParams{Wrapper::Client::getParameterDescriptors()}, - mClient{Wrapper::setParams(static_cast(this), mParams, mWorld->mVerbosity > 0, mWorld, - mControlsIterator, true)}, - mSynchronous{mNumInputs > 2 ? (in0(int(mNumInputs) - 1) > 0) : false} + : mSynchronous{mNumInputs > 2 ? (in0(int(mNumInputs) - 1) > 0) : false} {} ~NonRealTime() { - if (mClient.state() == ProcessState::kProcessing) + if (client().state() == ProcessState::kProcessing) { std::cout << Wrapper::getName() << ": Processing cancelled" << std::endl; Wrapper::getInterfaceTable()->fSendNodeReply(&mParent->mNode, 1, "/done", @@ -290,7 +282,7 @@ public: /// launches tidy up when complete void poll(int) { - out0(0) = mDone ? 1.0f : static_cast(mClient.progress()); + out0(0) = mDone ? 1.0f : static_cast(client().progress()); if (0 == pollCounter++ && !mCheckingForDone) { @@ -399,15 +391,13 @@ public: static_cast(unit)->mClient.cancel(); } - ParamSetType& params() - { - return mParams; - } + ParamSetType& params() { return mWrapper->mParams; } + Client& client() { return mWrapper->mClient; } private: - static Result validateParameters(NonRealTime* w) + static Result validateParameters(NonRealTime* nrt) { - auto results = w->mParams.constrainParameterValues(); + auto results = nrt->params().constrainParameterValues(); for (auto& r : results) { if (!r.ok()) return r; @@ -417,7 +407,7 @@ private: bool exchangeBuffers(World* world) // RT thread { - mParams.template forEachParamType(world); + params().template forEachParamType(world); // At this point, we can see if we're finished and let the language know (or // it can wait for the doneAction, but that takes extra time) use replyID to // convey status (0 = normal completion, 1 = cancelled) @@ -430,7 +420,7 @@ private: bool tidyUp(World*) // NRT thread { - mParams.template forEachParamType(); + params().template forEachParamType(); return true; } @@ -452,7 +442,6 @@ private: } }; - FloatControlsIter mControlsIterator; FifoMsg mFifoMsg; char* mCompletionMessage = nullptr; void* mReplyAddr = nullptr; @@ -461,12 +450,12 @@ private: index pollCounter{0}; protected: - ParamSetType mParams; - Client mClient; - bool mSynchronous{true}; - bool mQueueEnabled{false}; + bool mSynchronous{true}; + bool mQueueEnabled{false}; bool mCheckingForDone{false}; // only write to this from RT thread kthx bool mCancelled{false}; +private: + Wrapper* mWrapper{static_cast(this)}; }; //////////////////////////////////////////////////////////////////////////////// @@ -487,48 +476,169 @@ class NonRealTimeAndRealTime : public RealTime, //Discovery for clients that need persistent storage (Dataset and friends) +/// Named, shared clients already have a lookup table in their adaptor class template -struct IsPersistent : std::false_type -{}; +struct IsPersistent +{ + using type = std::false_type; +}; //TODO: make less tied to current implementation template struct IsPersistent>> - : std::true_type -{}; +{ + using type = std::true_type; +}; + +template +using IsPersistent_t = typename IsPersistent::type; + +/// Models don't, but still need to survive CMD-. +template +struct IsModel +{ + using type = std::false_type; +}; +template +struct IsModel>> +{ + using type = typename ClientWrapper::isModelObject; +}; + +template +using IsModel_t = typename IsModel::type; + +template +struct LifetimePolicy; -template -struct ObjectPersistance; +//template +//struct LifetimePolicy +//{ +//// static_assert(false,"Shared Objecthood and Model Objecthood are not compatible"); +//}; +/// Default policy template -struct ObjectPersistance{ - void init(){} +struct LifetimePolicy +{ + static void constructClass(Unit* unit) + { + FloatControlsIter controlsReader{unit->mInBuf + Wrapper::ControlOffset(unit),Wrapper::ControlSize(unit)}; + auto params = typename Wrapper::ParamSetType{Client::getParameterDescriptors()}; + Wrapper::setParams(unit, params, controlsReader); + Client client{params}; + new (static_cast(unit)) Wrapper(std::move(controlsReader), std::move(client), std::move(params)); + } + static void destroyClass(Unit* unit) { static_cast(unit)->~Wrapper(); } static void setup(InterfaceTable*, const char*){} }; +/// Model objects +template +struct LifetimePolicy +{ + + index uid; + + struct CacheRecord + { + typename Client::ParamSetType params{Client::getParameterDescriptors()}; + Client client{params}; + bool leased{false}; + }; + + using Cache = std::unordered_map; + + static void constructClass(Unit* unit) + { + index uid = static_cast(unit->mInBuf[Wrapper::ControlOffset(unit)][0]); + FloatControlsIter controlsReader{unit->mInBuf + 1 + Wrapper::ControlOffset(unit),Wrapper::ControlSize(unit)}; + auto& entry = mRegistry[uid]; + auto& client = entry.client; + auto& params = entry.params; + if(entry.leased) //if this happens, then the client has probably messed up + { + std::cout << "ERROR: ID " << uid << "is already being used by the cache" << std::endl; + return; + } + Wrapper::setParams(unit, entry.params,controlsReader); + new (static_cast(unit)) Wrapper{std::move(controlsReader),std::move(client),std::move(params)}; + static_cast(unit)->uid = uid; + entry.leased = true; + } + + static void destroyClass(Unit* unit) + { + auto wrapper = static_cast(unit); + index uid = wrapper->uid; + auto pos = mRegistry.find(uid); + if( pos != mRegistry.end() ) + { + //on cmd-. live to fight another day + auto& entry = *pos; + entry.second.client = std::move(wrapper->client()); + entry.second.params = std::move(wrapper->params()); + entry.second.leased = false; + } + wrapper->~Wrapper(); + } + + static void setup(InterfaceTable* ft, const char* name) + { + ft->fDefineUnitCmd(name, "free", [](Unit* unit, sc_msg_iter*) + { + //This ABSOLUTELY ASSUMES the client is going to also delete + //the Unit by calling Synth.free. + auto wrapper = static_cast(unit); + mRegistry.erase(wrapper->uid); + }); + } + +private: + static std::unordered_map mRegistry; +}; + template -struct ObjectPersistance +typename LifetimePolicy::Cache + LifetimePolicy::mRegistry{}; + + +/// Shared objects +template +struct LifetimePolicy { template struct GetSharedType; - + template struct GetSharedType>> { using type = NRTSharedInstanceAdaptor; }; - + using SharedType = typename GetSharedType::type; using ClientPointer = typename SharedType::ClientPointer; - void init() + static void constructClass(Unit* unit) { - auto clientRef = getClientPointer(static_cast(this)); + + FloatControlsIter controlsReader{unit->mInBuf + Wrapper::ControlOffset(unit),Wrapper::ControlSize(unit)}; + + auto params = typename Client::ParamSetType{Client::getParameterDescriptors()}; + Wrapper::setParams(unit, params,controlsReader); + auto& name = params.template get<0>(); + auto client = Client{params}; + auto clientRef = SharedType::lookup(name); + auto pos = mRegistry.find(clientRef); if(pos == mRegistry.end()) mRegistry.emplace(clientRef); + + new (static_cast(unit)) Wrapper(std::move(controlsReader),std::move(client),std::move(params)); + } - + static void destroyClass(Unit* unit) { static_cast(unit)->~Wrapper(); } + static void setup(InterfaceTable* ft, const char* name) { ft->fDefineUnitCmd(name, "free", [](Unit* unit, sc_msg_iter*) @@ -538,9 +648,7 @@ struct ObjectPersistance if(pos != mRegistry.end()) mRegistry.erase(clientRef); }); } - private: - static ClientPointer getClientPointer(Wrapper* wrapper) { auto& params = wrapper->params(); @@ -552,40 +660,44 @@ private: }; template -std::unordered_set::ClientPointer> - ObjectPersistance::mRegistry; -// Template Specialisations for NRT/RT +std::unordered_set::ClientPointer> + LifetimePolicy::mRegistry{}; + + +//// Template Specialisations for NRT/RT template class FluidSCWrapperImpl; template class FluidSCWrapperImpl - : public NonRealTime, public ObjectPersistance::value> + : public NonRealTime, + public LifetimePolicy, IsPersistent_t> { public: void init(){ NonRealTime::init(); - ObjectPersistance::value>::init(); } - static void setup(InterfaceTable* ft, const char* name){ + static void setup(InterfaceTable* ft, const char* name) + { NonRealTime::setup(ft,name); - ObjectPersistance::value>::setup(ft,name); + LifetimePolicy, IsPersistent_t>::setup(ft,name); } }; template class FluidSCWrapperImpl - : public RealTime, public ObjectPersistance::value> + : public RealTime, + public LifetimePolicy, IsPersistent_t> { public: void init(){ RealTime::init(); - ObjectPersistance::value>::init(); } - static void setup(InterfaceTable* ft, const char* name){ + static void setup(InterfaceTable* ft, const char* name) + { RealTime::setup(ft,name); - ObjectPersistance::value>::setup(ft,name); + LifetimePolicy, IsPersistent_t>::setup(ft,name); } }; @@ -608,6 +720,10 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase using FloatControlsIter = impl::FloatControlsIter; + //I would like to template these to something more scaleable, but baby steps + friend class impl::RealTime; + friend class impl::NonRealTime; + template struct ParamReader { @@ -692,20 +808,20 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase return tag == 's'; } - static auto fromArgs(FluidSCWrapper*, World*, sc_msg_iter* args, std::string, int) + static auto fromArgs(Unit*, sc_msg_iter* args, std::string, int) { const char* recv = args->gets(""); return std::string(recv ? recv : ""); } - static auto fromArgs(FluidSCWrapper*,World* w, FloatControlsIter& args, std::string, int) + static auto fromArgs(Unit* x, FloatControlsIter& args, std::string, int) { // first is string size, then chars index size = static_cast(args.next()); char* chunk = static_cast(FluidSCWrapper::getInterfaceTable()->fRTAlloc( - w, asUnsigned(size + 1))); + x->mWorld, asUnsigned(size + 1))); if (!chunk) { @@ -718,74 +834,74 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase chunk[i] = static_cast(args.next()); chunk[size] = 0; // terminate string - + //todo: Did I check that this is getting cleaned up somewhere? It doesn't + //look like it is return std::string{chunk}; } - template static std::enable_if_t::value, T> - fromArgs(FluidSCWrapper*,World*, FloatControlsIter& args, T, int) + fromArgs(Unit*, FloatControlsIter& args, T, int) { return static_cast(args.next()); } template static std::enable_if_t::value, T> - fromArgs(FluidSCWrapper*,World*, FloatControlsIter& args, T, int) + fromArgs(Unit*, FloatControlsIter& args, T, int) { return args.next(); } template static std::enable_if_t::value, T> - fromArgs(FluidSCWrapper*,World*, sc_msg_iter* args, T, int defVal) + fromArgs(Unit*, sc_msg_iter* args, T, int defVal) { return args->geti(defVal); } template static std::enable_if_t::value, T> - fromArgs(World*, sc_msg_iter* args, T, int) + fromArgs(Unit*, sc_msg_iter* args, T, int) { return args->getf(); } - static SCBufferAdaptor* fetchBuffer(FluidSCWrapper* x, World* w, index bufnum) + static SCBufferAdaptor* fetchBuffer(Unit* x, index bufnum) { - if(bufnum >= w->mNumSndBufs) + if(bufnum >= x->mWorld->mNumSndBufs) { - index localBufNum = bufnum - w->mNumSndBufs; + index localBufNum = bufnum - x->mWorld->mNumSndBufs; - Graph* parent = static_cast(x)->mParent; + Graph* parent = x->mParent; - return localBufNum <= parent -> localMaxBufNum ? - new SCBufferAdaptor(parent->mLocalSndBufs + localBufNum,w,true) + return localBufNum <= parent->localMaxBufNum ? + new SCBufferAdaptor(parent->mLocalSndBufs + localBufNum,x->mWorld,true) : nullptr; } else - return bufnum >= 0 ? new SCBufferAdaptor(bufnum, w) : nullptr; + return bufnum >= 0 ? new SCBufferAdaptor(bufnum, x->mWorld) : nullptr; } - static auto fromArgs(FluidSCWrapper* x,World* w, ArgType args, BufferT::type&, int) + static auto fromArgs(Unit* x, ArgType args, BufferT::type&, int) { typename LongT::type bufnum = static_cast( - ParamReader::fromArgs(x, w, args, typename LongT::type(), -1)); - return BufferT::type(fetchBuffer(x,w,bufnum)); + ParamReader::fromArgs(x, args, typename LongT::type(), -1)); + return BufferT::type(fetchBuffer(x, bufnum)); } - static auto fromArgs(FluidSCWrapper* x,World* w, ArgType args, InputBufferT::type&, int) + static auto fromArgs(Unit* x, ArgType args, InputBufferT::type&, int) { typename LongT::type bufnum = - static_cast(fromArgs(x, w, args, LongT::type(), -1)); - return InputBufferT::type(fetchBuffer(x,w,bufnum)); + static_cast(fromArgs(x, args, LongT::type(), -1)); + return InputBufferT::type(fetchBuffer(x, bufnum)); } template static std::enable_if_t::value, P> - fromArgs(FluidSCWrapper* x,World* w, ArgType args, P&, int) + fromArgs(Unit* x, ArgType args, P&, int) { - return {fromArgs(x, w, args, std::string{}, 0).c_str()}; + return {fromArgs(x, args, std::string{}, 0).c_str()}; } }; @@ -797,7 +913,7 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase static constexpr index argSize = C::getParameterDescriptors().template get().fixedSize; - typename T::type operator()(FluidSCWrapper* x, World* w, ArgType args) + typename T::type operator()(Unit* x, ArgType args) { // Just return default if there's nothing left to grab if (args.remain() == 0) @@ -813,7 +929,7 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase for (index i = 0; i < argSize; i++) a[i] = static_cast( - ParamReader::fromArgs(x, w, args, a[0], 0)); + ParamReader::fromArgs(x, args, a[0], 0)); return a.value(); } @@ -977,14 +1093,12 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase using ReturnType = typename MessageDescriptor::ReturnType; using IndexList = typename MessageDescriptor::IndexList; using MessageData = MessageDispatch; - auto ft = getInterfaceTable(); void* msgptr = ft->fRTAlloc(x->mWorld, sizeof(MessageData)); MessageData* msg = new (msgptr) MessageData; msg->name = '/' + Client::getMessageDescriptors().template name(); msg->wrapper = x; ArgTuple& args = msg->args; - // type check OSC message std::string tags(inArgs->tags + inArgs->count); @@ -1008,14 +1122,14 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase auto tagsIter = tags.begin(); auto tagsEnd = tags.end(); - ForEach(args,[&typesMatch,&tagsIter,&tagsEnd](auto& x){ + ForEach(args,[&typesMatch,&tagsIter,&tagsEnd](auto& arg){ if(tagsIter == tagsEnd) { typesMatch = false; return; } char t = *(tagsIter++); - typesMatch = typesMatch && ParamReader::argTypeOK(x,t); + typesMatch = typesMatch && ParamReader::argTypeOK(arg,t); }); willContinue = willContinue && typesMatch; @@ -1056,7 +1170,7 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase /// ForEach(args,[x,&inArgs](auto& arg){ - arg = ParamReader::fromArgs(x,x->mWorld,inArgs,arg,0); + arg = ParamReader::fromArgs(x, inArgs,arg,0); }); @@ -1108,8 +1222,6 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase return x->mClient.template invoke(x->mClient, std::get(args)...); } - - template // call from RT static void messageOutput(FluidSCWrapper* x, const std::string& s, MessageResult& result) @@ -1165,9 +1277,17 @@ class FluidSCWrapper : public impl::FluidSCWrapperBase public: using Client = C; - using ParameterSetType = typename C::ParamSetType; + using ParamSetType = typename C::ParamSetType; - FluidSCWrapper() { impl::FluidSCWrapperBase::init(); } + + + FluidSCWrapper(FloatControlsIter&& i, Client&& c, ParamSetType&& p): + mControlsIterator{std::move(i)}, + mParams{std::move(p)}, mClient{std::move(c)} + { + mClient.setParams(mParams); //<-IMPORTANT: client's ref to params is by address, and this has just changed + impl::FluidSCWrapperBase::init(); + } static const char* getName(const char* setName = nullptr) { @@ -1185,20 +1305,20 @@ public: { getName(name); getInterfaceTable(ft); + registerUnit(ft, name); impl::FluidSCWrapperBase::setup(ft, name); Client::getMessageDescriptors().template iterate(); ft->fDefineUnitCmd(name, "version", doVersion); } - static auto& setParams(FluidSCWrapper* x, ParameterSetType& p, bool verbose, World* world, + static auto& setParams(Unit* x, ParamSetType& p, FloatControlsIter& inputs, bool constrain = false) { //TODO: Regain this robustness if possible? - // We won't even try and set params if the arguments don't match // if (inputs.size() == C::getParameterDescriptors().count()) // { - p.template setParameterValues(verbose, x, world, inputs); + p.template setParameterValues(x->mWorld->mVerbosity > 0, x, inputs); if (constrain) p.constrainParameterValues(); // } // else @@ -1235,6 +1355,21 @@ public: } } } + + auto& client() { return mClient; } + auto& params() { return mParams; } + + private: + + static void registerUnit(InterfaceTable* ft, const char* name) { + UnitCtorFunc ctor =impl::FluidSCWrapperBase::constructClass; + UnitDtorFunc dtor = impl::FluidSCWrapperBase::destroyClass; + (*ft->fDefineUnit)(name, sizeof(FluidSCWrapper), ctor, dtor, 0); + } + + FloatControlsIter mControlsIterator; + ParamSetType mParams; + Client mClient; }; template