Skip to content

Commit

Permalink
Hot reloading the embedded js engine dsp
Browse files Browse the repository at this point in the history
  • Loading branch information
nick-thompson committed Nov 20, 2023
1 parent 16bf85c commit 3263ba3
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 51 deletions.
24 changes: 24 additions & 0 deletions dsp/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ globalThis.__receiveStateChange__ = (serializedState) => {
prevState = state;
};

// NOTE: This is highly experimental and should not yet be relied on
// as a consistent feature.
//
// This hook allows the native side to inject serialized graph state from
// the running elem::Runtime instance so that we can throw away and reinitialize
// the JavaScript engine and then inject necessary state for coordinating with
// the underlying engine.
globalThis.__receiveHydrationData__ = (data) => {
const payload = JSON.parse(data);
const nodeMap = core._delegate.nodeMap;

for (let [k, v] of Object.entries(payload)) {
nodeMap.set(parseInt(k, 16), {
symbol: '__ELEM_NODE__',
kind: '__HYDRATED__',
hash: parseInt(k, 16),
props: v,
generation: {
current: 0,
},
});
}
};

// Finally, an error callback which just logs back to native
globalThis.__receiveError__ = (err) => {
console.log(`[Error: ${err.name}] ${err.message}`);
Expand Down
121 changes: 71 additions & 50 deletions native/PluginProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,57 +205,10 @@ void EffectsPluginProcessor::handleAsyncUpdate()
// First things first, we check the flag to identify if we should initialize the Elementary
// runtime and engine.
if (shouldInitialize.exchange(false)) {
// TODO: This is definitely not thread-safe! It could delete a Runtime instance while
// the real-time thread is using it. Depends on when the host will call prepareToPlay.
runtime = std::make_unique<elem::Runtime<float>>(lastKnownSampleRate, lastKnownBlockSize);
jsContext = choc::javascript::createQuickJSContext();

// Install some native interop functions in our JavaScript environment
jsContext.registerFunction("__postNativeMessage__", [this](choc::javascript::ArgumentList args) {
auto const batch = elem::js::parseJSON(args[0]->toString());
auto const rc = runtime->applyInstructions(batch);

if (rc != elem::ReturnCode::Ok()) {
dispatchError("Runtime Error", elem::ReturnCode::describe(rc));
}

return choc::value::Value();
});

jsContext.registerFunction("__log__", [](choc::javascript::ArgumentList args) {
for (size_t i = 0; i < args.numArgs; ++i) {
DBG(choc::json::toString(*args[i], true));
}

return choc::value::Value();
});

// A simple shim to write various console operations to our native __log__ handler
jsContext.evaluate(R"shim(
(function() {
if (typeof globalThis.console === 'undefined') {
globalThis.console = {
log(...args) {
__log__('[log]', ...args);
},
warn(...args) {
__log__('[warn]', ...args);
},
error(...args) {
__log__('[error]', ...args);
}
};
}
})();
)shim");

// Load and evaluate our Elementary js main file
#if ELEM_DEV_LOCALHOST
auto dspEntryFile = juce::URL("http://localhost:5173/dsp.main.js");
auto dspEntryFileContents = dspEntryFile.readEntireTextStream().toStdString();
#else
auto dspEntryFile = getAssetsDirectory().getChildFile("dsp.main.js");
auto dspEntryFileContents = dspEntryFile.loadFileAsString().toStdString();
#endif
jsContext.evaluate(dspEntryFileContents);
initJavaScriptEngine();
}

// Next we iterate over the current parameter values to update our local state
Expand Down Expand Up @@ -285,6 +238,74 @@ void EffectsPluginProcessor::handleAsyncUpdate()
dispatchStateChange();
}

void EffectsPluginProcessor::initJavaScriptEngine()
{
jsContext = choc::javascript::createQuickJSContext();

// Install some native interop functions in our JavaScript environment
jsContext.registerFunction("__postNativeMessage__", [this](choc::javascript::ArgumentList args) {
auto const batch = elem::js::parseJSON(args[0]->toString());
auto const rc = runtime->applyInstructions(batch);

if (rc != elem::ReturnCode::Ok()) {
dispatchError("Runtime Error", elem::ReturnCode::describe(rc));
}

return choc::value::Value();
});

jsContext.registerFunction("__log__", [](choc::javascript::ArgumentList args) {
for (size_t i = 0; i < args.numArgs; ++i) {
DBG(choc::json::toString(*args[i], true));
}

return choc::value::Value();
});

// A simple shim to write various console operations to our native __log__ handler
jsContext.evaluate(R"shim(
(function() {
if (typeof globalThis.console === 'undefined') {
globalThis.console = {
log(...args) {
__log__('[log]', ...args);
},
warn(...args) {
__log__('[warn]', ...args);
},
error(...args) {
__log__('[error]', ...args);
}
};
}
})();
)shim");

// Load and evaluate our Elementary js main file
#if ELEM_DEV_LOCALHOST
auto dspEntryFile = juce::URL("http://localhost:5173/dsp.main.js");
auto dspEntryFileContents = dspEntryFile.readEntireTextStream().toStdString();
#else
auto dspEntryFile = getAssetsDirectory().getChildFile("dsp.main.js");
auto dspEntryFileContents = dspEntryFile.loadFileAsString().toStdString();
#endif
jsContext.evaluate(dspEntryFileContents);

// Re-hydrate from current state
const auto* kHydrateScript = R"script(
(function() {
if (typeof globalThis.__receiveHydrationData__ !== 'function')
return false;
globalThis.__receiveHydrationData__(%);
return true;
})();
)script";

auto expr = juce::String(kHydrateScript).replace("%", elem::js::serialize(elem::js::serialize(runtime->snapshot()))).toStdString();
jsContext.evaluate(expr);
}

void EffectsPluginProcessor::dispatchStateChange()
{
const auto* kDispatchScript = R"script(
Expand Down
3 changes: 3 additions & 0 deletions native/PluginProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class EffectsPluginProcessor
void handleAsyncUpdate() override;

//==============================================================================
/** Internal helper for initializing the embedded JS engine. */
void initJavaScriptEngine();

/** Internal helper for propagating processor state changes. */
void dispatchStateChange();
void dispatchError(std::string const& name, std::string const& message);
Expand Down
9 changes: 9 additions & 0 deletions native/WebViewEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ WebViewEditor::WebViewEditor(juce::AudioProcessor* proc, juce::File const& asset
}
}

#if ELEM_DEV_LOCALHOST
if (eventName == "reload") {
if (auto* ptr = dynamic_cast<EffectsPluginProcessor*>(getAudioProcessor())) {
ptr->initJavaScriptEngine();
ptr->dispatchStateChange();
}
}
#endif

if (eventName == "setParameterValue") {
jassert(args.size() > 1);
return handleSetParameterValueEvent(args[1]);
Expand Down
8 changes: 8 additions & 0 deletions src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function requestParamValueUpdate(paramId, value) {
}
}

import.meta.hot.on('reload-dsp', () => {
console.log('Sending reload dsp message');

if (typeof globalThis.__postNativeMessage__ === 'function') {
globalThis.__postNativeMessage__('reload');
}
});

globalThis.__receiveStateChange__ = function(state) {
store.setState(JSON.parse(state));
};
Expand Down
22 changes: 21 additions & 1 deletion vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ const currentCommit = execSync("git rev-parse --short HEAD").toString();
const date = new Date();
const dateString = `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;

// A helper plugin which specifically watches for changes to public/dsp.main.js,
// which is built in a parallel watch job via esbuild during dev.
//
// We can still use Vite's HMR to send a custom reload-dsp event, which is caught
// inside the webview and propagated to native to reinitialize the embedded js engine.
//
// During production builds, this all gets pruned from the bundle.
function pubDirReloadPlugin() {
return {
name: 'pubDirReload',
handleHotUpdate({file, server}) {
if (file.includes('public/dsp.main.js')) {
server.ws.send({
type: 'custom',
event: 'reload-dsp',
});
}
}
};
}

// https://vitejs.dev/config/
export default defineConfig({
Expand All @@ -15,5 +35,5 @@ export default defineConfig({
__COMMIT_HASH__: JSON.stringify(currentCommit),
__BUILD_DATE__: JSON.stringify(dateString),
},
plugins: [react()],
plugins: [react(), pubDirReloadPlugin()],
})

0 comments on commit 3263ba3

Please sign in to comment.