Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/rive/command_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "rive/command_queue.hpp"
#include "rive/hit_result.hpp"
#include <algorithm>
#include <iostream>
#include <sstream>
#include <thread>
Expand Down Expand Up @@ -173,6 +174,14 @@ class CommandServer

m_artboardDependencies.erase(dependencyItr);
}
for (auto& fileDependency : m_fileDependencies)
{
auto& artboardVector = fileDependency.second;
artboardVector.erase(std::remove(artboardVector.begin(),
artboardVector.end(),
Comment on lines +177 to +181
handle),
artboardVector.end());
}
m_artboards.erase(itr);
std::unique_lock<std::mutex> lock(m_commandQueue->m_messageMutex);
m_commandQueue->m_messageStream
Expand Down
20 changes: 14 additions & 6 deletions src/command_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -739,18 +739,23 @@ bool CommandServer::processCommands()
commandStream >> handle;
commandStream >> requestId;
lock.unlock();
m_files.erase(handle);
auto itr = m_fileDependencies.find(handle);
if (itr != m_fileDependencies.end())
{
auto& artboardVector = itr->second;
// Detach the dependency list before cascading:
// cleanupArtboard un-registers artboards from
// m_fileDependencies, which would otherwise mutate the
// vector while we iterate it.
auto artboardVector = std::move(itr->second);
m_fileDependencies.erase(itr);
for (auto artboardHandle : artboardVector)
{
cleanupArtboard(artboardHandle, requestId);
}

m_fileDependencies.erase(itr);
}
// Erase the file after the cascade; dependent artboards and
// state machines may reference file-owned data.
m_files.erase(handle);
std::unique_lock<std::mutex> messageLock(
m_commandQueue->m_messageMutex);
messageStream << CommandQueue::Message::fileDeleted;
Expand Down Expand Up @@ -1022,6 +1027,9 @@ bool CommandServer::processCommands()
{
m_artboardDependencies[handle] = {};
m_artboards[handle] = std::move(artboard);
assert(m_fileDependencies.find(fileHandle) !=
m_fileDependencies.end());
m_fileDependencies[fileHandle].push_back(handle);
Comment on lines +1030 to +1032

std::unique_lock<std::mutex> messageLock(
m_commandQueue->m_messageMutex);
Expand Down Expand Up @@ -1172,9 +1180,9 @@ bool CommandServer::processCommands()
commandStream >> handle;
commandStream >> requestId;
lock.unlock();
// cleanupArtboard also un-registers the artboard from
// m_fileDependencies.
cleanupArtboard(handle, requestId);
// We don't remove from the file dependencies here because
// calling erase on a non existent key is fine.
break;
}

Expand Down
109 changes: 109 additions & 0 deletions tests/unit_tests/runtime/command_queue_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,115 @@ TEST_CASE("state machine management", "[CommandQueue]")
serverThread.join();
}

TEST_CASE("deleteFile cascade-deletes live artboards and their state machines",
"[CommandQueue]")
{
auto commandQueue = make_rcp<CommandQueue>();
std::thread serverThread(server_thread, commandQueue);

std::ifstream stream("assets/multiple_state_machines.riv",
std::ios::binary);
FileHandle fileHandle = commandQueue->loadFile(
std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
ArtboardHandle artboardHandle1 =
commandQueue->instantiateDefaultArtboard(fileHandle);
ArtboardHandle artboardHandle2 =
commandQueue->instantiateDefaultArtboard(fileHandle);
StateMachineHandle sm1 =
commandQueue->instantiateStateMachineNamed(artboardHandle1, "one");
StateMachineHandle sm2 =
commandQueue->instantiateStateMachineNamed(artboardHandle2, "two");
commandQueue->runOnce([fileHandle,
artboardHandle1,
artboardHandle2,
sm1,
sm2](CommandServer* server) {
REQUIRE(server->getFile(fileHandle) != nullptr);
REQUIRE(server->getArtboardInstance(artboardHandle1) != nullptr);
REQUIRE(server->getArtboardInstance(artboardHandle2) != nullptr);
REQUIRE(server->getStateMachineInstance(sm1) != nullptr);
REQUIRE(server->getStateMachineInstance(sm2) != nullptr);
});

// Delete the file WITHOUT deleting its artboards or state machines first.
// The file's dependency cascade must clean all of them up.
commandQueue->deleteFile(fileHandle);
commandQueue->runOnce([fileHandle,
artboardHandle1,
artboardHandle2,
sm1,
sm2](CommandServer* server) {
CHECK(server->getFile(fileHandle) == nullptr);
CHECK(server->getArtboardInstance(artboardHandle1) == nullptr);
CHECK(server->getArtboardInstance(artboardHandle2) == nullptr);
CHECK(server->getStateMachineInstance(sm1) == nullptr);
CHECK(server->getStateMachineInstance(sm2) == nullptr);
});

commandQueue->disconnect();
serverThread.join();
}

TEST_CASE("explicit deletes before deleteFile do not double-clean",
"[CommandQueue]")
{
auto commandQueue = make_rcp<CommandQueue>();
std::thread serverThread(server_thread, commandQueue);

std::ifstream stream("assets/multiple_state_machines.riv",
std::ios::binary);
FileHandle fileHandle = commandQueue->loadFile(
std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
ArtboardHandle artboardHandle =
commandQueue->instantiateDefaultArtboard(fileHandle);
StateMachineHandle sm =
commandQueue->instantiateStateMachineNamed(artboardHandle, "one");

commandQueue->deleteStateMachine(sm);
commandQueue->deleteArtboard(artboardHandle);
commandQueue->deleteFile(fileHandle);
commandQueue->runOnce(
[fileHandle, artboardHandle, sm](CommandServer* server) {
CHECK(server->getFile(fileHandle) == nullptr);
CHECK(server->getArtboardInstance(artboardHandle) == nullptr);
CHECK(server->getStateMachineInstance(sm) == nullptr);
});

commandQueue->disconnect();
serverThread.join();
}

TEST_CASE("commands on cascade-deleted handles are safely rejected",
"[CommandQueue]")
{
auto commandQueue = make_rcp<CommandQueue>();
std::thread serverThread(server_thread, commandQueue);

std::ifstream stream("assets/multiple_state_machines.riv",
std::ios::binary);
FileHandle fileHandle = commandQueue->loadFile(
std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
ArtboardHandle artboardHandle =
commandQueue->instantiateDefaultArtboard(fileHandle);
StateMachineHandle sm =
commandQueue->instantiateStateMachineNamed(artboardHandle, "one");

commandQueue->deleteFile(fileHandle);

// The client still holds handles to the cascade-deleted objects; using
// them must route through the normal unknown-handle error path.
commandQueue->advanceStateMachine(sm, 0.016f);
commandQueue->deleteStateMachine(sm);
commandQueue->deleteArtboard(artboardHandle);
commandQueue->runOnce([artboardHandle, sm](CommandServer* server) {
CHECK(server->getArtboardInstance(artboardHandle) == nullptr);
CHECK(server->getStateMachineInstance(sm) == nullptr);
});

commandQueue->disconnect();
serverThread.join();
}

TEST_CASE("default artboard & state machine", "[CommandQueue]")
{
auto commandQueue = make_rcp<CommandQueue>();
Expand Down