diff --git a/dsp/main.js b/dsp/main.js index 0eba395..9560abb 100644 --- a/dsp/main.js +++ b/dsp/main.js @@ -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}`); diff --git a/native/PluginProcessor.cpp b/native/PluginProcessor.cpp index c39fc9b..fe249ca 100644 --- a/native/PluginProcessor.cpp +++ b/native/PluginProcessor.cpp @@ -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>(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 @@ -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( diff --git a/native/PluginProcessor.h b/native/PluginProcessor.h index 659e412..d828257 100644 --- a/native/PluginProcessor.h +++ b/native/PluginProcessor.h @@ -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); diff --git a/native/WebViewEditor.cpp b/native/WebViewEditor.cpp index 59a3d9b..eccf13a 100644 --- a/native/WebViewEditor.cpp +++ b/native/WebViewEditor.cpp @@ -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(getAudioProcessor())) { + ptr->initJavaScriptEngine(); + ptr->dispatchStateChange(); + } + } +#endif + if (eventName == "setParameterValue") { jassert(args.size() > 1); return handleSetParameterValueEvent(args[1]); diff --git a/src/main.jsx b/src/main.jsx index 2066f05..92711d3 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -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)); }; diff --git a/vite.config.js b/vite.config.js index 071095a..1ac4369 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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({ @@ -15,5 +35,5 @@ export default defineConfig({ __COMMIT_HASH__: JSON.stringify(currentCommit), __BUILD_DATE__: JSON.stringify(dateString), }, - plugins: [react()], + plugins: [react(), pubDirReloadPlugin()], })