Split Module::bypass() into isBypass/setBypass(). Add more documentation to Engine methods.tags/v2.0.0
@@ -13,9 +13,9 @@ namespace engine { | |||
/** Manages Modules and Cables and steps them in time. | |||
All methods are thread-safe and can safely be called from anywhere. | |||
However, the following methods obtain a write-lock, so they cannot be called with the engine thread (e.g. inside Module::process()): | |||
clear, stepBlock, setPrimaryModule, setSampleRate, addModule, removeModule, resetModule, randomizeModule, bypassModule, moduleFromJson, addCable, removeCable, addParamHandle, removeParamHandle, and fromJson | |||
Engine contains a read/write mutex that locks when the Engine state is being read or written (manipulated). | |||
Methods that read-lock (stated in their documentation) can be called simultaneously with other read-locking methods. | |||
Methods that write-lock cannot be called simultaneously or recursively with another read-locking or write-locking method. | |||
*/ | |||
struct Engine { | |||
struct Internal; | |||
@@ -24,15 +24,20 @@ struct Engine { | |||
Engine(); | |||
~Engine(); | |||
/** Removes all modules and cables. */ | |||
/** Removes all modules and cables. | |||
Write-locks. | |||
*/ | |||
void clear(); | |||
PRIVATE void clear_NoLock(); | |||
/** Advances the engine by `frames` frames. | |||
Only call this method from the primary module. | |||
Read-locks. Also locks so only one stepBlock() can be called simultaneously or recursively. | |||
*/ | |||
void stepBlock(int frames); | |||
/** Module does not need to belong to the Engine. | |||
However, Engine will unset the primary module when it is removed from the Engine. | |||
NULL will unset the primary module. | |||
Write-locks. | |||
*/ | |||
void setPrimaryModule(Module* module); | |||
Module* getPrimaryModule(); | |||
@@ -40,7 +45,13 @@ struct Engine { | |||
/** Returns the sample rate used by the engine for stepping each module. | |||
*/ | |||
float getSampleRate(); | |||
/** Sets the sample rate to step the modules. | |||
Write-locks. | |||
*/ | |||
PRIVATE void setSampleRate(float sampleRate); | |||
/** Sets the sample rate if the sample rate in the settings is "Auto". | |||
Write-locks. | |||
*/ | |||
void setSuggestedSampleRate(float suggestedSampleRate); | |||
/** Returns the inverse of the current sample rate. | |||
*/ | |||
@@ -78,37 +89,85 @@ struct Engine { | |||
/** Fills `moduleIds` with up to `len` module IDs in the rack. | |||
Returns the number of IDs written. | |||
This C-like method does no allocations. The vector C++ version below does. | |||
Read-locks. | |||
*/ | |||
size_t getModuleIds(int64_t* moduleIds, size_t len); | |||
/** Returns a vector of module IDs in the rack. | |||
Read-locks. | |||
*/ | |||
std::vector<int64_t> getModuleIds(); | |||
/** Adds a module to the rack engine. | |||
The module ID must not be taken by another module. | |||
/** Adds a Module to the rack. | |||
The module ID must not be taken by another Module. | |||
If the module ID is -1, an ID is automatically assigned. | |||
Does not transfer pointer ownership. | |||
Write-locks. | |||
*/ | |||
void addModule(Module* module); | |||
/** Removes a Module from the rack. | |||
Write-locks. | |||
*/ | |||
void removeModule(Module* module); | |||
PRIVATE void removeModule_NoLock(Module* module); | |||
/** Checks whether a Module is in the rack. | |||
Read-locks. | |||
*/ | |||
bool hasModule(Module* module); | |||
/** Returns the Module with the given ID in the rack. | |||
Read-locks. | |||
*/ | |||
Module* getModule(int64_t moduleId); | |||
/** Triggers a ResetEvent for the given Module. | |||
Write-locks. | |||
*/ | |||
void resetModule(Module* module); | |||
/** Triggers a RandomizeEvent for the given Module. | |||
Write-locks. | |||
*/ | |||
void randomizeModule(Module* module); | |||
/** Sets the bypass state and triggers a BypassEvent or UnBypassEvent of the given Module. | |||
Write-locks. | |||
*/ | |||
void bypassModule(Module* module, bool bypass); | |||
/** Serializes/deserializes with locking, ensuring that Module::process() is not called during toJson()/fromJson(). | |||
/** Serializes the given Module with locking, ensuring that Module::process() is not called simultaneously. | |||
Read-locks. | |||
*/ | |||
json_t* moduleToJson(Module* module); | |||
/** Serializes the given Module with locking, ensuring that Module::process() is not called simultaneously. | |||
Write-locks. | |||
*/ | |||
void moduleFromJson(Module* module, json_t* rootJ); | |||
// Cables | |||
size_t getNumCables(); | |||
/** Fills `cableIds` with up to `len` cable IDs in the rack. | |||
Returns the number of IDs written. | |||
This C-like method does no allocations. The vector C++ version below does. | |||
Read-locks. | |||
*/ | |||
size_t getCableIds(int64_t* cableIds, size_t len); | |||
/** Returns a vector of cable IDs in the rack. | |||
Read-locks. | |||
*/ | |||
std::vector<int64_t> getCableIds(); | |||
/** Adds a cable to the rack engine. | |||
/** Adds a Cable to the rack. | |||
The cable ID must not be taken by another cable. | |||
If the cable ID is -1, an ID is automatically assigned. | |||
Does not transfer pointer ownership. | |||
Write-locks. | |||
*/ | |||
void addCable(Cable* cable); | |||
/** Removes a Cable from the rack. | |||
Write-locks. | |||
*/ | |||
void removeCable(Cable* cable); | |||
PRIVATE void removeCable_NoLock(Cable* cable); | |||
/** Checks whether a Cable is in the rack. | |||
Read-locks. | |||
*/ | |||
bool hasCable(Cable* cable); | |||
/** Returns the Cable with the given ID in the rack. | |||
Read-locks. | |||
*/ | |||
Cable* getCable(int64_t cableId); | |||
// Params | |||
@@ -122,19 +181,37 @@ struct Engine { | |||
float getSmoothParam(Module* module, int paramId); | |||
// ParamHandles | |||
/** Adds a ParamHandle to the rack. | |||
Does not automatically update the ParamHandle. | |||
Write-locks. | |||
*/ | |||
void addParamHandle(ParamHandle* paramHandle); | |||
/** | |||
Write-locks. | |||
*/ | |||
void removeParamHandle(ParamHandle* paramHandle); | |||
PRIVATE void removeParamHandle_NoLock(ParamHandle* paramHandle); | |||
/** Returns the unique ParamHandle for the given paramId | |||
Read-locks. | |||
*/ | |||
ParamHandle* getParamHandle(int64_t moduleId, int paramId); | |||
/** Use getParamHandle(moduleId, paramId) instead. */ | |||
/** Use getParamHandle(moduleId, paramId) instead. | |||
Read-locks. | |||
*/ | |||
DEPRECATED ParamHandle* getParamHandle(Module* module, int paramId); | |||
/** Sets the ParamHandle IDs and module pointer. | |||
If `overwrite` is true and another ParamHandle points to the same param, unsets that one and replaces it with the given handle. | |||
Read-locks. | |||
*/ | |||
void updateParamHandle(ParamHandle* paramHandle, int64_t moduleId, int paramId, bool overwrite = true); | |||
/** Serializes the rack. | |||
Read-locks. | |||
*/ | |||
json_t* toJson(); | |||
/** Deserializes the rack. | |||
Write-locks. | |||
*/ | |||
void fromJson(json_t* rootJ); | |||
}; | |||
@@ -344,7 +344,8 @@ struct Module { | |||
/** DEPRECATED. Override `onSampleRateChange(e)` instead. */ | |||
virtual void onSampleRateChange() {} | |||
PRIVATE bool& bypass(); | |||
bool isBypass(); | |||
PRIVATE void setBypass(bool bypass); | |||
PRIVATE const float* meterBuffer(); | |||
PRIVATE int meterLength(); | |||
PRIVATE int meterIndex(); | |||
@@ -100,7 +100,7 @@ | |||
#undef PRIVATE | |||
#define PRIVATE __attribute__((warning ("Using private function or symbol"))) | |||
#define PRIVATE __attribute__((error ("Using private function or symbol"))) | |||
namespace rack { | |||
@@ -394,7 +394,7 @@ ModuleWidget::~ModuleWidget() { | |||
void ModuleWidget::draw(const DrawArgs& args) { | |||
nvgScissor(args.vg, RECT_ARGS(args.clipBox)); | |||
if (module && module->bypass()) { | |||
if (module && module->isBypass()) { | |||
nvgGlobalAlpha(args.vg, 0.33); | |||
} | |||
@@ -1002,12 +1002,12 @@ void ModuleWidget::cloneAction() { | |||
void ModuleWidget::bypassAction() { | |||
assert(module); | |||
APP->engine->bypassModule(module, !module->bypass()); | |||
APP->engine->bypassModule(module, !module->isBypass()); | |||
// history::ModuleBypass | |||
history::ModuleBypass* h = new history::ModuleBypass; | |||
h->moduleId = module->id; | |||
h->bypass = module->bypass(); | |||
h->bypass = module->isBypass(); | |||
APP->history->push(h); | |||
} | |||
@@ -1075,7 +1075,7 @@ void ModuleWidget::createContextMenu() { | |||
ModuleBypassItem* bypassItem = new ModuleBypassItem; | |||
bypassItem->text = "Bypass"; | |||
bypassItem->rightText = RACK_MOD_CTRL_NAME "+E"; | |||
if (module && module->bypass()) | |||
if (module && module->isBypass()) | |||
bypassItem->rightText = CHECKMARK_STRING " " + bypassItem->rightText; | |||
bypassItem->moduleWidget = this; | |||
menu->addChild(bypassItem); | |||
@@ -32,15 +32,11 @@ static void initMXCSR() { | |||
/** Allows multiple "reader" threads to obtain a lock simultaneously, but only one "writer" thread. | |||
WriteLock can be used recursively. | |||
Uses reader priority, which implies that ReadLock can also be used recursively. | |||
This implementation is just a wrapper for pthreads. | |||
This implementation is currently just a wrapper for pthreads, which works on Linux/Mac/. | |||
This is available in C++17 as std::shared_mutex, but unfortunately we're using C++11. | |||
*/ | |||
struct ReadWriteMutex { | |||
pthread_rwlock_t rwlock; | |||
std::atomic<pthread_t> writerThread{0}; | |||
int recursion = 0; | |||
ReadWriteMutex() { | |||
if (pthread_rwlock_init(&rwlock, NULL)) | |||
@@ -58,21 +54,12 @@ struct ReadWriteMutex { | |||
throw Exception("pthread_rwlock_unlock failed"); | |||
} | |||
void lockWriter() { | |||
pthread_t self = pthread_self(); | |||
if (writerThread != self) { | |||
if (pthread_rwlock_wrlock(&rwlock)) | |||
throw Exception("pthread_rwlock_wrlock failed"); | |||
writerThread = self; | |||
} | |||
// We're safe behind a lock so we can increment the recursion count. | |||
recursion++; | |||
if (pthread_rwlock_wrlock(&rwlock)) | |||
throw Exception("pthread_rwlock_wrlock failed"); | |||
} | |||
void unlockWriter() { | |||
if (--recursion == 0) { | |||
writerThread = 0; | |||
if (pthread_rwlock_unlock(&rwlock)) | |||
throw Exception("pthread_rwlock_unlock failed"); | |||
} | |||
if (pthread_rwlock_unlock(&rwlock)) | |||
throw Exception("pthread_rwlock_unlock failed"); | |||
} | |||
}; | |||
@@ -210,9 +197,6 @@ struct EngineWorker { | |||
}; | |||
static const float FALLBACK_SAMPLE_RATE = 44100; | |||
struct Engine::Internal { | |||
std::vector<Module*> modules; | |||
std::vector<Cable*> cables; | |||
@@ -240,7 +224,7 @@ struct Engine::Internal { | |||
float smoothValue = 0.f; | |||
/** Mutex that guards the Engine state, such as settings, Modules, and Cables. | |||
Writers lock when mutating the engine's state. | |||
Writers lock when mutating the engine's state or stepping the block. | |||
Readers lock when using the engine's state. | |||
*/ | |||
ReadWriteMutex mutex; | |||
@@ -483,7 +467,7 @@ Engine::Engine() { | |||
internal = new Internal; | |||
internal->context = contextGet(); | |||
setSampleRate(FALLBACK_SAMPLE_RATE); | |||
setSuggestedSampleRate(0.f); | |||
} | |||
@@ -507,21 +491,25 @@ Engine::~Engine() { | |||
void Engine::clear() { | |||
WriteLock lock(internal->mutex); | |||
clear_NoLock(); | |||
} | |||
void Engine::clear_NoLock() { | |||
// Copy lists because we'll be removing while iterating | |||
std::set<ParamHandle*> paramHandles = internal->paramHandles; | |||
for (ParamHandle* paramHandle : paramHandles) { | |||
removeParamHandle(paramHandle); | |||
// Don't delete paramHandle because they're owned by other things (e.g. Modules) | |||
removeParamHandle_NoLock(paramHandle); | |||
// Don't delete paramHandle because they're normally owned by Module subclasses | |||
} | |||
std::vector<Cable*> cables = internal->cables; | |||
for (Cable* cable : cables) { | |||
removeCable(cable); | |||
removeCable_NoLock(cable); | |||
delete cable; | |||
} | |||
std::vector<Module*> modules = internal->modules; | |||
for (Module* module : modules) { | |||
removeModule(module); | |||
removeModule_NoLock(module); | |||
delete module; | |||
} | |||
} | |||
@@ -604,7 +592,8 @@ void Engine::setSuggestedSampleRate(float suggestedSampleRate) { | |||
setSampleRate(suggestedSampleRate); | |||
} | |||
else { | |||
setSampleRate(FALLBACK_SAMPLE_RATE); | |||
// Fallback sample rate | |||
setSampleRate(44100.f); | |||
} | |||
} | |||
@@ -711,6 +700,11 @@ void Engine::addModule(Module* module) { | |||
void Engine::removeModule(Module* module) { | |||
WriteLock lock(internal->mutex); | |||
removeModule_NoLock(module); | |||
} | |||
void Engine::removeModule_NoLock(Module* module) { | |||
assert(module); | |||
// Check that the module actually exists | |||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module); | |||
@@ -792,7 +786,7 @@ void Engine::bypassModule(Module* module, bool bypass) { | |||
WriteLock lock(internal->mutex); | |||
assert(module); | |||
if (module->bypass() == bypass) | |||
if (module->isBypass() == bypass) | |||
return; | |||
// Clear outputs and set to 1 channel | |||
for (Output& output : module->outputs) { | |||
@@ -800,7 +794,7 @@ void Engine::bypassModule(Module* module, bool bypass) { | |||
output.setChannels(0); | |||
} | |||
// Set bypass state | |||
module->bypass() = bypass; | |||
module->setBypass(bypass); | |||
// Trigger event | |||
if (bypass) { | |||
Module::BypassEvent eBypass; | |||
@@ -901,6 +895,11 @@ void Engine::addCable(Cable* cable) { | |||
void Engine::removeCable(Cable* cable) { | |||
WriteLock lock(internal->mutex); | |||
removeCable_NoLock(cable); | |||
} | |||
void Engine::removeCable_NoLock(Cable* cable) { | |||
assert(cable); | |||
// Check that the cable is already added | |||
auto it = std::find(internal->cables.begin(), internal->cables.end(), cable); | |||
@@ -935,6 +934,14 @@ void Engine::removeCable(Cable* cable) { | |||
} | |||
bool Engine::hasCable(Cable* cable) { | |||
ReadLock lock(internal->mutex); | |||
// TODO Performance could be improved by searching cablesCache, but more testing would be needed to make sure it's always valid. | |||
auto it = std::find(internal->cables.begin(), internal->cables.end(), cable); | |||
return it != internal->cables.end(); | |||
} | |||
Cable* Engine::getCable(int64_t cableId) { | |||
ReadLock lock(internal->mutex); | |||
auto it = internal->cablesCache.find(cableId); | |||
@@ -996,6 +1003,11 @@ void Engine::addParamHandle(ParamHandle* paramHandle) { | |||
void Engine::removeParamHandle(ParamHandle* paramHandle) { | |||
WriteLock lock(internal->mutex); | |||
removeParamHandle_NoLock(paramHandle); | |||
} | |||
void Engine::removeParamHandle_NoLock(ParamHandle* paramHandle) { | |||
// Check that the ParamHandle is already added | |||
auto it = internal->paramHandles.find(paramHandle); | |||
assert(it != internal->paramHandles.end()); | |||
@@ -1035,6 +1047,7 @@ void Engine::updateParamHandle(ParamHandle* paramHandle, int64_t moduleId, int p | |||
if (paramHandle->moduleId >= 0) { | |||
// Replace old ParamHandle, or reset the current ParamHandle | |||
// TODO Maybe call getParamHandle_NoLock()? | |||
ParamHandle* oldParamHandle = getParamHandle(moduleId, paramId); | |||
if (oldParamHandle) { | |||
if (overwrite) { | |||
@@ -1052,6 +1065,7 @@ void Engine::updateParamHandle(ParamHandle* paramHandle, int64_t moduleId, int p | |||
// Set module pointer if the above block didn't reset it | |||
if (paramHandle->moduleId >= 0) { | |||
// TODO Maybe call getModule_NoLock()? | |||
paramHandle->module = getModule(paramHandle->moduleId); | |||
} | |||
@@ -1086,8 +1100,9 @@ json_t* Engine::toJson() { | |||
void Engine::fromJson(json_t* rootJ) { | |||
// Don't write-lock here because most of this function doesn't need it. | |||
// Don't write-lock the entire method because most of it doesn't need it. | |||
// Write-locks | |||
clear(); | |||
// modules | |||
json_t* modulesJ = json_object_get(rootJ, "modules"); | |||
@@ -1108,7 +1123,7 @@ void Engine::fromJson(json_t* rootJ) { | |||
module->id = moduleIndex; | |||
} | |||
// This write-locks | |||
// Write-locks | |||
addModule(module); | |||
} | |||
catch (Exception& e) { | |||
@@ -1131,7 +1146,7 @@ void Engine::fromJson(json_t* rootJ) { | |||
Cable* cable = new Cable; | |||
try { | |||
cable->fromJson(cableJ); | |||
// This write-locks | |||
// Write-locks | |||
addCable(cable); | |||
} | |||
catch (Exception& e) { | |||
@@ -282,11 +282,16 @@ void Module::onRandomize(const RandomizeEvent& e) { | |||
} | |||
bool& Module::bypass() { | |||
bool Module::isBypass() { | |||
return internal->bypass; | |||
} | |||
void Module::setBypass(bool bypass) { | |||
internal->bypass = bypass; | |||
} | |||
const float* Module::meterBuffer() { | |||
return internal->meterBuffer; | |||
} | |||