Skip to content
Merged
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
22 changes: 22 additions & 0 deletions packages/qvac-lib-infer-stable-diffusion-cpp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.vs/
build/
dist/
models/
store/
node_modules/
prebuilds/

.npmrc
package-lock.json
.cache/
.idea/
**/store/
.DS_Store
logs/
*.gguf
*.safetensors
*.ckpt
*.log
.clang-tidy
# Added by qvac-lint-cpp
.clang-format
119 changes: 119 additions & 0 deletions packages/qvac-lib-infer-stable-diffusion-cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
cmake_minimum_required(VERSION 3.25)

option(ANDROID_STL "Android STL linkage" c++_shared)
option(BUILD_TESTING "Build tests" OFF)
if(BUILD_TESTING)
list(APPEND VCPKG_MANIFEST_FEATURES "tests")
endif()

find_package(cmake-bare REQUIRED PATHS node_modules/cmake-bare)
find_package(cmake-vcpkg REQUIRED PATHS node_modules/cmake-vcpkg)

project(qvac-lib-inference-addon-sd C CXX)

find_path(VCPKG_INSTALLED_PATH share/qvac-lint-cpp/.clang-format REQUIRED)
configure_file(${VCPKG_INSTALLED_PATH}/share/qvac-lint-cpp/.clang-format
${CMAKE_CURRENT_SOURCE_DIR}/.clang-format COPYONLY)
configure_file(${VCPKG_INSTALLED_PATH}/share/qvac-lint-cpp/.clang-tidy
${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy COPYONLY)

find_path(PICOJSON_INCLUDE_DIRS "picojson/picojson.h")
find_path(QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS "qvac-lib-inference-addon-cpp/JsInterface.hpp")

# stable-diffusion.cpp – uses the CMake config installed by the overlay port
find_package(stable-diffusion-cpp CONFIG REQUIRED)

# stb headers are installed into the same include dir by the port
find_path(STB_IMAGE_WRITE_INCLUDE_DIR "stb_image_write.h" REQUIRED)

if(WIN32)
add_definitions(-DNOMINMAX -DWIN32_MEAN_AND_LEAN -DNOGDI)
endif()

bare_target(bare_target_value)
bare_module_target("." unused_target NAME module_name VERSION unused_version)
set(BACKENDS_SUBDIR_VALUE "${bare_target_value}/${module_name}")
message("Building qvac-lib-inference-addon-sd with BACKENDS_SUBDIR='${BACKENDS_SUBDIR_VALUE}'")

# On Linux/Android, install ggml dynamic backend libraries alongside the addon
set(BACKEND_DL_LIBS "")
if((ANDROID OR UNIX) AND NOT APPLE)
# ggml backends are built inside stable-diffusion.cpp port
foreach(_backend ${GGML_AVAILABLE_BACKENDS})
list(APPEND BACKEND_DL_LIBS INSTALL TARGET ggml::${_backend})
endforeach()
endif()

add_bare_module(qvac-lib-inference-addon-sd EXPORTS ${BACKEND_DL_LIBS})
set(ADDON_SOURCES
${PROJECT_SOURCE_DIR}/addon/src/js-interface/binding.cpp
${PROJECT_SOURCE_DIR}/addon/src/model-interface/SdModel.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/LoggingMacros.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/BackendSelection.cpp
)

target_sources(
${qvac-lib-inference-addon-sd}
PRIVATE
${ADDON_SOURCES}
)

target_include_directories(
${qvac-lib-inference-addon-sd}
PRIVATE
${PICOJSON_INCLUDE_DIRS}
${QVAC_LIB_INFERENCE_ADDON_CPP_INCLUDE_DIRS}
${STB_IMAGE_WRITE_INCLUDE_DIR}
${PROJECT_SOURCE_DIR}/addon/src
)

target_link_libraries(
${qvac-lib-inference-addon-sd}
PRIVATE
stable-diffusion::stable-diffusion
)

# Link Metal framework on Apple platforms
if(APPLE)
find_library(FOUNDATION_LIBRARY Foundation REQUIRED)
find_library(METAL_LIBRARY Metal REQUIRED)
find_library(METALKIT_LIBRARY MetalKit REQUIRED)
find_library(METALPERFORMANCESHADERS_LIBRARY MetalPerformanceShaders)
target_link_libraries(
${qvac-lib-inference-addon-sd}
PRIVATE
${FOUNDATION_LIBRARY}
${METAL_LIBRARY}
${METALKIT_LIBRARY}
)
if(METALPERFORMANCESHADERS_LIBRARY)
target_link_libraries(${qvac-lib-inference-addon-sd} PRIVATE ${METALPERFORMANCESHADERS_LIBRARY})
endif()
endif()

# Link OpenCL on Android
if(ANDROID)
find_package(opencl CONFIG)
if(opencl_FOUND)
target_link_libraries(${qvac-lib-inference-addon-sd} PRIVATE opencl)
endif()
endif()

target_compile_features(${qvac-lib-inference-addon-sd} PRIVATE cxx_std_20)
target_compile_definitions(${qvac-lib-inference-addon-sd} PUBLIC JS_LOGGER)
target_compile_definitions(${qvac-lib-inference-addon-sd} PRIVATE BACKENDS_SUBDIR="${BACKENDS_SUBDIR_VALUE}")

if(BUILD_TESTING)
find_package(GTest CONFIG REQUIRED)
include(GoogleTest)
enable_testing()
add_subdirectory(test/unit)
endif()

if(WIN32)
target_link_libraries(
${qvac-lib-inference-addon-sd}
PRIVATE
msvcrt.lib
)
endif()
70 changes: 70 additions & 0 deletions packages/qvac-lib-infer-stable-diffusion-cpp/addon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict'

const path = require('bare-path')

/**
* JavaScript wrapper around the native stable-diffusion.cpp addon.
* Manages the native handle lifecycle and bridges JS ↔ C++.
*/
class SdInterface {
/**
* @param {object} binding - The native addon binding (from require.addon())
* @param {object} configurationParams - Configuration for the SD context
* @param {string} configurationParams.path - Local file path to the model weights
* @param {object} [configurationParams.config] - SD-specific configuration options
* @param {Function} outputCb - Called on any generation event (started, progress, output, error)
*/
constructor (binding, configurationParams, outputCb) {
this._binding = binding

if (!configurationParams.config) {
configurationParams.config = {}
}

if (!configurationParams.config.backendsDir) {
configurationParams.config.backendsDir = path.join(__dirname, 'prebuilds')
}

this._handle = this._binding.createInstance(
this,
configurationParams,
outputCb
)
}

/**
* Moves addon to the LISTENING state after initialization.
*/
async activate () {
this._binding.activate(this._handle)
}

/**
* Cancel the current generation job.
*/
async cancel () {
if (!this._handle) return
await this._binding.cancel(this._handle)
}

/**
* Run a generation job with the given parameters.
* @param {object} params - Generation parameters (will be JSON-serialized)
* @returns {Promise<boolean>} true if job was accepted, false if busy
*/
async runJob (params) {
const paramsJson = JSON.stringify(params)
return this._binding.runJob(this._handle, [{ type: 'text', input: paramsJson }])
}

/**
* Unload the model and release all native resources.
*/
async unload () {
if (!this._handle) return
this._binding.destroyInstance(this._handle)
this._handle = null
}
}

module.exports = { SdInterface }
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#pragma once

#include <memory>
#include <vector>

#include <qvac-lib-inference-addon-cpp/JsInterface.hpp>
#include <qvac-lib-inference-addon-cpp/JsUtils.hpp>
#include <qvac-lib-inference-addon-cpp/ModelInterfaces.hpp>
#include <qvac-lib-inference-addon-cpp/addon/AddonJs.hpp>
#include <qvac-lib-inference-addon-cpp/handlers/JsOutputHandlerImplementations.hpp>
#include <qvac-lib-inference-addon-cpp/handlers/OutputHandler.hpp>
#include <qvac-lib-inference-addon-cpp/queue/OutputCallbackJs.hpp>

#include "model-interface/SdModel.hpp"

namespace qvac_lib_inference_addon_sd {

inline js_value_t* createInstance(js_env_t* env, js_callback_info_t* info) try {
using namespace qvac_lib_inference_addon_cpp;
using namespace std;

JsArgsParser args(env, info);

// Extract configuration from JS object at args[1]
const string modelPath = args.getMapEntry(1, "path");
const string clipLPath = args.getMapEntry(1, "clipLPath");
const string clipGPath = args.getMapEntry(1, "clipGPath");
const string t5XxlPath = args.getMapEntry(1, "t5XxlPath");
const string vaePath = args.getMapEntry(1, "vaePath");
auto configMap = args.getSubmap(1, "config");

auto model = make_unique<SdModel>(
modelPath, clipLPath, clipGPath, t5XxlPath, vaePath, std::move(configMap));

// Register output handlers for both progress strings and image byte arrays
out_handl::OutputHandlers<out_handl::JsOutputHandlerInterface> outHandlers;
outHandlers.add(make_shared<out_handl::JsStringOutputHandler>());
outHandlers.add(make_shared<out_handl::JsTypedArrayOutputHandler<uint8_t>>());

unique_ptr<OutputCallBackInterface> callback = make_unique<OutputCallBackJs>(
env,
args.get(0, "jsHandle"),
args.getFunction(2, "outputCallback"),
std::move(outHandlers));

auto addon = make_unique<AddonJs>(env, std::move(callback), std::move(model));

return JsInterface::createInstance(env, std::move(addon));
}
JSCATCH

inline js_value_t* runJob(js_env_t* env, js_callback_info_t* info) try {
using namespace qvac_lib_inference_addon_cpp;
using namespace std;

JsArgsParser args(env, info);
AddonJs& instance = JsInterface::getInstance(env, args.get(0, "instance"));

auto [type, jsInput] = JsInterface::getInput(args);

if (type != "text") {
throw StatusError(
general_error::InvalidArgument,
"stable-diffusion runJob expects a single text input with JSON params");
}

const string paramsJson =
js::String(env, jsInput).as<std::string>(env);

SdModel::GenerationJob job;
job.paramsJson = paramsJson;

// Queue step-progress updates as JSON strings (handled by JsStringOutputHandler)
job.progressCallback = [&instance](const std::string& progressJson) {
instance.addonCpp->outputQueue->queueResult(std::any(progressJson));
};

// Queue final image/frame bytes (handled by JsTypedArrayOutputHandler<uint8_t>)
job.outputCallback = [&instance](const std::vector<uint8_t>& imageBytes) {
instance.addonCpp->outputQueue->queueResult(std::any(imageBytes));
};

return instance.runJob(std::any(std::move(job)));
}
JSCATCH

} // namespace qvac_lib_inference_addon_sd
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include <bare.h>

#include "../addon/AddonJs.hpp"

js_value_t*
qvacLibInferenceAddonSdExports(js_env_t* env, js_value_t* exports) {

// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define V(name, fn) \
{ \
js_value_t* val; \
if (js_create_function(env, name, -1, fn, nullptr, &val) != 0) { \
return nullptr; \
} \
if (js_set_named_property(env, exports, name, val) != 0) { \
return nullptr; \
} \
}

V("createInstance", qvac_lib_inference_addon_sd::createInstance)
V("runJob", qvac_lib_inference_addon_sd::runJob)

V("activate", qvac_lib_inference_addon_cpp::JsInterface::activate)
V("cancel", qvac_lib_inference_addon_cpp::JsInterface::cancel)
V("destroyInstance", qvac_lib_inference_addon_cpp::JsInterface::destroyInstance)
V("setLogger", qvac_lib_inference_addon_cpp::JsInterface::setLogger)
V("releaseLogger", qvac_lib_inference_addon_cpp::JsInterface::releaseLogger)

#undef V
return exports;
}

BARE_MODULE(qvac_lib_inference_addon_sd, qvacLibInferenceAddonSdExports)
Loading