diff --git a/dev/core_generator/lib/src/definition.dart b/dev/core_generator/lib/src/definition.dart index c9607c94e..8dc61f203 100644 --- a/dev/core_generator/lib/src/definition.dart +++ b/dev/core_generator/lib/src/definition.dart @@ -153,6 +153,9 @@ class Definition { property.type.snakeRuntimeCoreName + '.hpp'); } + if (properties.any((p) => p.isAtomic)) { + includes.add(''); + } var sortedIncludes = includes.toList()..sort(); for (final include in sortedIncludes) { @@ -229,12 +232,24 @@ class Definition { var initialize = property.initialValueRuntime ?? property.initialValue ?? property.type.defaultValue; - String fieldLine = - '${property.type.cppName} m_${property.capitalizedName}'; - if (initialize != null) { - var converted = property.type.convertCpp(initialize); - if (converted != null) { - fieldLine += ' = $converted'; + String fieldLine; + if (property.isAtomic) { + fieldLine = + 'std::atomic<${property.type.cppName}> m_${property.capitalizedName}'; + if (initialize != null) { + var converted = property.type.convertCpp(initialize); + if (converted != null) { + fieldLine += '{$converted}'; + } + } + } else { + fieldLine = + '${property.type.cppName} m_${property.capitalizedName}'; + if (initialize != null) { + var converted = property.type.convertCpp(initialize); + if (converted != null) { + fieldLine += ' = $converted'; + } } } code.writeln('$fieldLine;'); @@ -287,6 +302,27 @@ class Definition { 'set${property.capitalizedName}(value);' '${property.name}Changed();' '}'); + } else if (property.isAtomic) { + code.writeln('inline' + ' ${property.type.cppGetterName} ${property.name}() const ' + + (property.isGetOverride ? 'override' : '') + + '{ return m_${property.capitalizedName}' + '.load(std::memory_order_relaxed); }'); + code.writeln( + 'void ${property.name}(${property.type.cppName} value) ' + + (property.isSetOverride ? 'override' : '') + + '{' + 'if(m_${property.capitalizedName}' + '.load(std::memory_order_relaxed) == value)' + '{return;}' + 'm_${property.capitalizedName}' + '.store(value, std::memory_order_relaxed);' + '${property.name}Changed();' + '}'); + code.writeln( + 'std::atomic<${property.type.cppName}>* ' + 'atomic${property.capitalizedName}() ' + '{ return &m_${property.capitalizedName}; }'); } else { code.writeln(((property.isVirtual || property.isPureVirtual) ? 'virtual' @@ -329,6 +365,11 @@ class Definition { } if (property.isEncoded) { code.writeln('copy${property.capitalizedName}(object);'); + } else if (property.isAtomic) { + code.writeln('m_${property.capitalizedName}.store(' + 'object.m_${property.capitalizedName}' + '.load(std::memory_order_relaxed), ' + 'std::memory_order_relaxed);'); } else { code.writeln('m_${property.capitalizedName} = ' 'object.m_${property.capitalizedName};'); @@ -357,6 +398,10 @@ class Definition { if (property.isEncoded) { code.writeln('decode${property.capitalizedName}' '(${property.type.runtimeCoreType}::deserialize(reader));'); + } else if (property.isAtomic) { + code.writeln('m_${property.capitalizedName}.store(' + '${property.type.runtimeCoreType}::deserialize(reader), ' + 'std::memory_order_relaxed);'); } else { code.writeln('m_${property.capitalizedName} = ' '${property.type.runtimeCoreType}::deserialize(reader);'); diff --git a/dev/core_generator/lib/src/property.dart b/dev/core_generator/lib/src/property.dart index 3ee834d61..ed534edfe 100644 --- a/dev/core_generator/lib/src/property.dart +++ b/dev/core_generator/lib/src/property.dart @@ -24,6 +24,7 @@ class Property { bool isBindable = false; bool isPassthrough = false; bool isPureVirtual = false; + bool isAtomic = false; FieldType? typeRuntime; static Property? make( @@ -113,6 +114,10 @@ class Property { if (pv is bool) { isPureVirtual = pv; } + dynamic at = data['atomic']; + if (at is bool) { + isAtomic = at; + } key = Key.fromJSON(data['key']) ?? Key.forProperty(this); } diff --git a/dev/defs/viewmodel/viewmodel_instance_number.json b/dev/defs/viewmodel/viewmodel_instance_number.json index e0637f9ec..b0846e900 100644 --- a/dev/defs/viewmodel/viewmodel_instance_number.json +++ b/dev/defs/viewmodel/viewmodel_instance_number.json @@ -13,7 +13,8 @@ "int": 575, "string": "propertyvalue" }, - "description": "The number value." + "description": "The number value.", + "atomic": true }, "playbackValue": { "type": "double", diff --git a/include/rive/command_queue.hpp b/include/rive/command_queue.hpp index 77051af51..176bdf1b1 100644 --- a/include/rive/command_queue.hpp +++ b/include/rive/command_queue.hpp @@ -9,9 +9,11 @@ #include "rive/math/vec2d.hpp" #include "rive/viewmodel/runtime/viewmodel_runtime.hpp" +#include #include #include #include +#include #include #include #include @@ -59,6 +61,21 @@ struct ViewModelEnum std::vector enumerants; }; +struct NumberReader +{ + std::atomic*> ptr{nullptr}; + + float value() const + { + auto p = ptr.load(std::memory_order_acquire); + return p ? p->load(std::memory_order_relaxed) : 0.0f; + } + bool isReady() const + { + return ptr.load(std::memory_order_acquire) != nullptr; + } +}; + // Client-side recorder for commands that will be executed by a // CommandServer. class CommandQueue : public RefCnt @@ -269,6 +286,12 @@ class CommandQueue : public RefCnt std::string path, size_t size) {} + + virtual void onViewModelInstanceNameReceived( + const ViewModelInstanceHandle, + uint64_t requestId, + std::string name) + {} }; class StateMachineListener @@ -631,11 +654,23 @@ class CommandQueue : public RefCnt std::string path, uint64_t requestId = 0); + void requestViewModelInstanceName(ViewModelInstanceHandle, + uint64_t requestId = 0); + void requestStateMachineNames(ArtboardHandle, uint64_t requestId = 0); void requestDefaultViewModelInfo(ArtboardHandle, FileHandle, uint64_t requestId = 0); + std::shared_ptr createNumberReader( + ViewModelInstanceHandle, + std::string path, + uint64_t requestId = 0); + + void releaseNumberReader(ViewModelInstanceHandle, + std::string path, + uint64_t requestId = 0); + // Consume all messages received from the server. void processMessages(); @@ -668,6 +703,11 @@ class CommandQueue : public RefCnt m_globalFontListener = listener; } + void setMessageAvailableCallback(std::function callback) + { + m_messageAvailableCallback = std::move(callback); + } + private: void registerListener(FileHandle handle, FileListener* listener) { @@ -826,7 +866,10 @@ class CommandQueue : public RefCnt listViewModelInstanceNames, listViewModelProperties, listViewModelPropertyValue, - getViewModelListSize + getViewModelListSize, + getViewModelInstanceName, + createNumberReader, + releaseNumberReader }; enum class Message @@ -842,6 +885,7 @@ class CommandQueue : public RefCnt viewModelPropertiesListed, viewModelPropertyValueReceived, viewModelListSizeReceived, + viewModelInstanceNameReceived, fileLoaded, fileDeleted, imageDecoded, @@ -886,6 +930,7 @@ class CommandQueue : public RefCnt ObjectStream m_names; ObjectStream m_callbacks; ObjectStream m_drawCallbacks; + ObjectStream> m_numberReaderPtrs; // Messages streams std::mutex m_messageMutex; @@ -912,6 +957,8 @@ class CommandQueue : public RefCnt m_viewModelListeners; std::unordered_map m_stateMachineListeners; + + std::function m_messageAvailableCallback; }; }; // namespace rive diff --git a/include/rive/command_server.hpp b/include/rive/command_server.hpp index 2f217f55b..b1d9f45a4 100644 --- a/include/rive/command_server.hpp +++ b/include/rive/command_server.hpp @@ -6,6 +6,7 @@ #include "rive/command_queue.hpp" #include +#include #include #include #include @@ -126,6 +127,7 @@ class CommandServer }; void checkPropertySubscriptions(); + void notifyMessageAvailable(); Vec2D cursorPosForPointerEvent(StateMachineInstance*, const CommandQueue::PointerEvent&); @@ -174,6 +176,15 @@ class CommandServer // because we iterate through the entire vector every frame anyway. std::vector m_propertySubscriptions; + struct NumberReaderEntry + { + std::shared_ptr reader; + ViewModelInstanceHandle viewModelHandle; + std::string propertyPath; + rcp valueRef; + }; + std::vector m_numberReaders; + // Dependencies // When a file gets deleted artboards and statemachine become invalid. Here // we hold a reference to the artboard only because that artboard has a diff --git a/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp index 57c235651..524e89a20 100644 --- a/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp +++ b/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp @@ -1,5 +1,6 @@ #ifndef _RIVE_VIEW_MODEL_INSTANCE_NUMBER_BASE_HPP_ #define _RIVE_VIEW_MODEL_INSTANCE_NUMBER_BASE_HPP_ +#include #include "rive/core/field_types/core_double_type.hpp" #include "rive/viewmodel/viewmodel_instance_value.hpp" namespace rive @@ -31,24 +32,30 @@ class ViewModelInstanceNumberBase : public ViewModelInstanceValue static const uint16_t propertyValuePropertyKey = 575; protected: - float m_PropertyValue = 0.0f; + std::atomic m_PropertyValue{0.0f}; public: - inline float propertyValue() const { return m_PropertyValue; } + inline float propertyValue() const + { + return m_PropertyValue.load(std::memory_order_relaxed); + } void propertyValue(float value) { - if (m_PropertyValue == value) + if (m_PropertyValue.load(std::memory_order_relaxed) == value) { return; } - m_PropertyValue = value; + m_PropertyValue.store(value, std::memory_order_relaxed); propertyValueChanged(); } + std::atomic* atomicPropertyValue() { return &m_PropertyValue; } Core* clone() const override; void copy(const ViewModelInstanceNumberBase& object) { - m_PropertyValue = object.m_PropertyValue; + m_PropertyValue.store( + object.m_PropertyValue.load(std::memory_order_relaxed), + std::memory_order_relaxed); ViewModelInstanceValue::copy(object); } @@ -57,7 +64,8 @@ class ViewModelInstanceNumberBase : public ViewModelInstanceValue switch (propertyKey) { case propertyValuePropertyKey: - m_PropertyValue = CoreDoubleType::deserialize(reader); + m_PropertyValue.store(CoreDoubleType::deserialize(reader), + std::memory_order_relaxed); return true; } return ViewModelInstanceValue::deserialize(propertyKey, reader); @@ -68,4 +76,4 @@ class ViewModelInstanceNumberBase : public ViewModelInstanceValue }; } // namespace rive -#endif \ No newline at end of file +#endif diff --git a/src/command_queue.cpp b/src/command_queue.cpp index 2c0d6955f..21c119a27 100644 --- a/src/command_queue.cpp +++ b/src/command_queue.cpp @@ -998,6 +998,16 @@ void CommandQueue::requestViewModelInstanceListSize( m_names << path; } +void CommandQueue::requestViewModelInstanceName( + ViewModelInstanceHandle handle, + uint64_t requestId) +{ + AutoLockAndNotify lock(m_commandMutex, m_commandConditionVariable); + m_commandStream << Command::getViewModelInstanceName; + m_commandStream << handle; + m_commandStream << requestId; +} + void CommandQueue::requestStateMachineNames(ArtboardHandle artboardHandle, uint64_t requestId) { @@ -1018,6 +1028,32 @@ void CommandQueue::requestDefaultViewModelInfo(ArtboardHandle artboardHandle, m_commandStream << requestId; } +std::shared_ptr CommandQueue::createNumberReader( + ViewModelInstanceHandle handle, + std::string path, + uint64_t requestId) +{ + auto reader = std::make_shared(); + AutoLockAndNotify lock(m_commandMutex, m_commandConditionVariable); + m_commandStream << Command::createNumberReader; + m_commandStream << handle; + m_commandStream << requestId; + m_names << std::move(path); + m_numberReaderPtrs << reader; + return reader; +} + +void CommandQueue::releaseNumberReader(ViewModelInstanceHandle handle, + std::string path, + uint64_t requestId) +{ + AutoLockAndNotify lock(m_commandMutex, m_commandConditionVariable); + m_commandStream << Command::releaseNumberReader; + m_commandStream << handle; + m_commandStream << requestId; + m_names << std::move(path); +} + void CommandQueue::processMessages() { std::unique_lock lock(m_messageMutex); @@ -1379,6 +1415,33 @@ void CommandQueue::processMessages() break; } + case Message::viewModelInstanceNameReceived: + { + ViewModelInstanceHandle handle; + std::string name; + uint64_t requestId; + m_messageStream >> handle; + m_messageStream >> requestId; + m_messageNames >> name; + lock.unlock(); + if (m_globalViewModelListener) + { + m_globalViewModelListener + ->onViewModelInstanceNameReceived(handle, + requestId, + name); + } + auto itr = m_viewModelListeners.find(handle); + if (itr != m_viewModelListeners.end()) + { + itr->second->onViewModelInstanceNameReceived( + handle, + requestId, + std::move(name)); + } + break; + } + case Message::fileLoaded: { FileHandle handle; diff --git a/src/command_server.cpp b/src/command_server.cpp index 051091602..1b5087f17 100644 --- a/src/command_server.cpp +++ b/src/command_server.cpp @@ -10,6 +10,7 @@ #include "rive/assets/image_asset.hpp" #include "rive/assets/script_asset.hpp" #include "rive/file.hpp" +#include "rive/viewmodel/viewmodel_instance_number.hpp" #include "rive/viewmodel/runtime/viewmodel_runtime.hpp" namespace rive @@ -2624,6 +2625,40 @@ bool CommandServer::processCommands() break; } + case CommandQueue::Command::getViewModelInstanceName: + { + ViewModelInstanceHandle handle; + uint64_t requestId; + commandStream >> handle; + commandStream >> requestId; + lock.unlock(); + + if (auto viewModel = getViewModelInstance(handle)) + { + std::unique_lock messageLock( + m_commandQueue->m_messageMutex); + messageStream + << CommandQueue::Message:: + viewModelInstanceNameReceived; + messageStream << handle; + messageStream << requestId; + m_commandQueue->m_messageNames << viewModel->name(); + } + else + { + ErrorReporter( + this, + handle, + requestId, + CommandQueue::Message::viewModelError) + << "failed to get view model instance " + << handle + << " when getting instance name"; + } + + break; + } + case CommandQueue::Command::pointerMove: { StateMachineHandle handle; @@ -2825,10 +2860,98 @@ bool CommandServer::processCommands() break; } + case CommandQueue::Command::createNumberReader: + { + ViewModelInstanceHandle rootHandle; + uint64_t requestId; + std::string path; + std::shared_ptr reader; + commandStream >> rootHandle; + commandStream >> requestId; + m_commandQueue->m_names >> path; + m_commandQueue->m_numberReaderPtrs >> reader; + lock.unlock(); + + if (auto view = getViewModelInstance(rootHandle)) + { + if (auto prop = view->propertyNumber(path)) + { + auto* instVal = prop->viewModelInstanceValue(); + auto* numInst = + instVal->as(); + auto* atomicPtr = numInst->atomicPropertyValue(); + reader->ptr.store(atomicPtr, + std::memory_order_release); + m_numberReaders.push_back( + {std::move(reader), + rootHandle, + std::move(path), + ref_rcp(instVal)}); + } + else + { + ErrorReporter( + this, + rootHandle, + requestId, + CommandQueue::Message::viewModelError) + << "Property not found when creating " + "number reader: " + << path; + } + } + else + { + ErrorReporter( + this, + rootHandle, + requestId, + CommandQueue::Message::viewModelError) + << "View model not found when creating " + "number reader"; + } + break; + } + + case CommandQueue::Command::releaseNumberReader: + { + ViewModelInstanceHandle rootHandle; + uint64_t requestId; + std::string path; + commandStream >> rootHandle; + commandStream >> requestId; + m_commandQueue->m_names >> path; + lock.unlock(); + + auto it = std::remove_if( + m_numberReaders.begin(), + m_numberReaders.end(), + [&](NumberReaderEntry& entry) { + if (entry.viewModelHandle == rootHandle && + entry.propertyPath == path) + { + entry.reader->ptr.store( + nullptr, + std::memory_order_release); + return true; + } + return false; + }); + m_numberReaders.erase(it, m_numberReaders.end()); + break; + } + case CommandQueue::Command::disconnect: { lock.unlock(); + for (auto& entry : m_numberReaders) + { + entry.reader->ptr.store(nullptr, + std::memory_order_release); + } + m_numberReaders.clear(); m_wasDisconnectReceived = true; + notifyMessageAvailable(); return false; } } @@ -2850,7 +2973,16 @@ bool CommandServer::processCommands() m_uniqueDraws.clear(); checkPropertySubscriptions(); + notifyMessageAvailable(); return !m_wasDisconnectReceived; } + +void CommandServer::notifyMessageAvailable() +{ + if (m_commandQueue->m_messageAvailableCallback) + { + m_commandQueue->m_messageAvailableCallback(); + } +} }; // namespace rive diff --git a/tests/unit_tests/runtime/command_queue_test.cpp b/tests/unit_tests/runtime/command_queue_test.cpp index a9b2b22de..d95df98e1 100644 --- a/tests/unit_tests/runtime/command_queue_test.cpp +++ b/tests/unit_tests/runtime/command_queue_test.cpp @@ -8,6 +8,7 @@ #include "rive/command_server.hpp" #include "rive/file.hpp" #include "common/render_context_null.hpp" +#include #include namespace rive @@ -4541,4 +4542,240 @@ TEST_CASE("dependency lifetime management", "[CommandQueue]") commandQueue->disconnect(); serverThread.join(); -} \ No newline at end of file +} + +TEST_CASE("messageAvailableCallback fires on messages", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::atomic callbackCount{0}; + commandQueue->setMessageAvailableCallback( + [&callbackCount]() { callbackCount++; }); + + std::thread serverThread(server_thread, commandQueue); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {}), + nullptr, + 1); + + wait_for_server(commandQueue.get()); + CHECK(callbackCount > 0); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("messageAvailableCallback fires on disconnect", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::atomic callbackCount{0}; + commandQueue->setMessageAvailableCallback( + [&callbackCount]() { callbackCount++; }); + + std::thread serverThread(server_thread, commandQueue); + + wait_for_server(commandQueue.get()); + int countBeforeDisconnect = callbackCount.load(); + + commandQueue->disconnect(); + serverThread.join(); + + CHECK(callbackCount > countBeforeDisconnect); +} + +TEST_CASE("no crash when messageAvailableCallback not set", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::thread serverThread(server_thread, commandQueue); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {}), + nullptr, + 1); + + wait_for_server(commandQueue.get()); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("NumberReader reads value set by server", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::unique_ptr nullContext = + RenderContextNULL::MakeContext(); + CommandServer server(commandQueue, nullContext.get()); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {})); + + auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); + auto vmHandle = + commandQueue->instantiateDefaultViewModelInstance(fileHandle, + artboardHandle); + + auto reader = commandQueue->createNumberReader(vmHandle, "Test Num"); + + server.processCommands(); + + CHECK(reader->isReady()); + float initial = reader->value(); + + commandQueue->setViewModelInstanceNumber(vmHandle, "Test Num", 42.0f); + server.processCommands(); + + CHECK(reader->value() == 42.0f); + + commandQueue->setViewModelInstanceNumber(vmHandle, "Test Num", -1.5f); + server.processCommands(); + + CHECK(reader->value() == -1.5f); + + commandQueue->disconnect(); + server.processCommands(); +} + +TEST_CASE("NumberReader release invalidates pointer", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::unique_ptr nullContext = + RenderContextNULL::MakeContext(); + CommandServer server(commandQueue, nullContext.get()); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {})); + + auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); + auto vmHandle = + commandQueue->instantiateDefaultViewModelInstance(fileHandle, + artboardHandle); + + auto reader = commandQueue->createNumberReader(vmHandle, "Test Num"); + server.processCommands(); + CHECK(reader->isReady()); + + commandQueue->releaseNumberReader(vmHandle, "Test Num"); + server.processCommands(); + CHECK_FALSE(reader->isReady()); + CHECK(reader->value() == 0.0f); + + commandQueue->disconnect(); + server.processCommands(); +} + +TEST_CASE("NumberReader invalid path stays not ready", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::unique_ptr nullContext = + RenderContextNULL::MakeContext(); + CommandServer server(commandQueue, nullContext.get()); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {})); + + auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); + auto vmHandle = + commandQueue->instantiateDefaultViewModelInstance(fileHandle, + artboardHandle); + + auto reader = + commandQueue->createNumberReader(vmHandle, "Nonexistent Property"); + server.processCommands(); + commandQueue->processMessages(); + + CHECK_FALSE(reader->isReady()); + CHECK(reader->value() == 0.0f); + + commandQueue->disconnect(); + server.processCommands(); +} + +TEST_CASE("NumberReader disconnect cleans up", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::thread serverThread(server_thread, commandQueue); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {})); + + auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); + auto vmHandle = + commandQueue->instantiateDefaultViewModelInstance(fileHandle, + artboardHandle); + + auto reader = commandQueue->createNumberReader(vmHandle, "Test Num"); + wait_for_server(commandQueue.get()); + CHECK(reader->isReady()); + + commandQueue->disconnect(); + serverThread.join(); + + CHECK_FALSE(reader->isReady()); +} + +TEST_CASE("NumberReader nested property path", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::unique_ptr nullContext = + RenderContextNULL::MakeContext(); + CommandServer server(commandQueue, nullContext.get()); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {})); + + auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); + auto vmHandle = + commandQueue->instantiateDefaultViewModelInstance(fileHandle, + artboardHandle); + + auto reader = commandQueue->createNumberReader(vmHandle, + "Test Nested/Nested Number"); + server.processCommands(); + CHECK(reader->isReady()); + + commandQueue->setViewModelInstanceNumber(vmHandle, + "Test Nested/Nested Number", + 99.0f); + server.processCommands(); + CHECK(reader->value() == 99.0f); + + commandQueue->disconnect(); + server.processCommands(); +} + +TEST_CASE("NumberReader async thread", "[CommandQueue]") +{ + auto commandQueue = make_rcp(); + std::thread serverThread(server_thread, commandQueue); + + std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector(std::istreambuf_iterator(stream), {})); + + auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle); + auto vmHandle = + commandQueue->instantiateDefaultViewModelInstance(fileHandle, + artboardHandle); + + auto reader = commandQueue->createNumberReader(vmHandle, "Test Num"); + wait_for_server(commandQueue.get()); + CHECK(reader->isReady()); + + commandQueue->setViewModelInstanceNumber(vmHandle, "Test Num", 123.0f); + wait_for_server(commandQueue.get()); + CHECK(reader->value() == 123.0f); + + commandQueue->releaseNumberReader(vmHandle, "Test Num"); + wait_for_server(commandQueue.get()); + CHECK_FALSE(reader->isReady()); + + commandQueue->disconnect(); + serverThread.join(); +}