From 1fb3ecc485ce966ca0da5e97bc1138dce5bae3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 13 Feb 2026 15:31:34 +0100 Subject: [PATCH 1/2] feat(command_queue): add message-available callback Add setMessageAvailableCallback() to CommandQueue so the server can notify the client immediately when response messages are available, instead of relying on polling via processMessages() on the render loop. --- include/rive/command_queue.hpp | 7 +++ include/rive/command_server.hpp | 1 + src/command_server.cpp | 10 ++++ .../unit_tests/runtime/command_queue_test.cpp | 60 ++++++++++++++++++- 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/include/rive/command_queue.hpp b/include/rive/command_queue.hpp index 77051af51..8f2f11636 100644 --- a/include/rive/command_queue.hpp +++ b/include/rive/command_queue.hpp @@ -668,6 +668,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) { @@ -912,6 +917,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..284595cf6 100644 --- a/include/rive/command_server.hpp +++ b/include/rive/command_server.hpp @@ -126,6 +126,7 @@ class CommandServer }; void checkPropertySubscriptions(); + void notifyMessageAvailable(); Vec2D cursorPosForPointerEvent(StateMachineInstance*, const CommandQueue::PointerEvent&); diff --git a/src/command_server.cpp b/src/command_server.cpp index 051091602..6aa3a3dd0 100644 --- a/src/command_server.cpp +++ b/src/command_server.cpp @@ -2829,6 +2829,7 @@ bool CommandServer::processCommands() { lock.unlock(); m_wasDisconnectReceived = true; + notifyMessageAvailable(); return false; } } @@ -2850,7 +2851,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..b1b7900a5 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,61 @@ 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(); +} From b9deded606ff69bca9af73b4a2a6221b4ccbd73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 19 Feb 2026 09:22:20 +0100 Subject: [PATCH 2/2] feat(command_queue): add NumberReader and VMI name query Add atomic NumberReader for zero-latency reads of ViewModel number properties, and add requestViewModelInstanceName command to retrieve VMI names from the server. --- dev/core_generator/lib/src/definition.dart | 57 +++++- dev/core_generator/lib/src/property.dart | 5 + .../viewmodel/viewmodel_instance_number.json | 3 +- include/rive/command_queue.hpp | 42 +++- include/rive/command_server.hpp | 10 + .../viewmodel_instance_number_base.hpp | 22 ++- src/command_queue.cpp | 63 ++++++ src/command_server.cpp | 122 ++++++++++++ .../unit_tests/runtime/command_queue_test.cpp | 179 ++++++++++++++++++ 9 files changed, 488 insertions(+), 15 deletions(-) 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 8f2f11636..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(); @@ -831,7 +866,10 @@ class CommandQueue : public RefCnt listViewModelInstanceNames, listViewModelProperties, listViewModelPropertyValue, - getViewModelListSize + getViewModelListSize, + getViewModelInstanceName, + createNumberReader, + releaseNumberReader }; enum class Message @@ -847,6 +885,7 @@ class CommandQueue : public RefCnt viewModelPropertiesListed, viewModelPropertyValueReceived, viewModelListSizeReceived, + viewModelInstanceNameReceived, fileLoaded, fileDeleted, imageDecoded, @@ -891,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; diff --git a/include/rive/command_server.hpp b/include/rive/command_server.hpp index 284595cf6..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 @@ -175,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 6aa3a3dd0..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,9 +2860,96 @@ 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; diff --git a/tests/unit_tests/runtime/command_queue_test.cpp b/tests/unit_tests/runtime/command_queue_test.cpp index b1b7900a5..d95df98e1 100644 --- a/tests/unit_tests/runtime/command_queue_test.cpp +++ b/tests/unit_tests/runtime/command_queue_test.cpp @@ -4600,3 +4600,182 @@ TEST_CASE("no crash when messageAvailableCallback not set", "[CommandQueue]") 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(); +}