Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
490b6ae
QVAC-17990 Add standalone ESRGAN upscaler API
gabrielgrigoras-serv May 5, 2026
5efbfae
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 5, 2026
86baad7
QVAC-17990 Address standalone upscaler review feedback
gabrielgrigoras-serv May 5, 2026
00dde92
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 5, 2026
12da56c
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 5, 2026
7b7f919
QVAC-17990 Raise error when upscaler thread detection fails
gabrielgrigoras-serv May 6, 2026
237b776
QVAC-17990 Share diffusion backend loading
gabrielgrigoras-serv May 6, 2026
7da8cc5
QVAC-17990 Honor cancel during ESRGAN upscale
gabrielgrigoras-serv May 6, 2026
4aab1c9
QVAC-17990 Tighten upscaler validation
gabrielgrigoras-serv May 6, 2026
5ccb2e7
QVAC-17990 Add ESRGAN e2e integration coverage
gabrielgrigoras-serv May 6, 2026
2392739
QVAC-17990 Add standalone upscaler changelog and model links
gabrielgrigoras-serv May 6, 2026
2982390
QVAC-17990 Add ESRGAN coexistence integration coverage
gabrielgrigoras-serv May 6, 2026
8c80613
QVAC-17990 Share ESRGAN helpers and warn on dropped output
gabrielgrigoras-serv May 7, 2026
16da200
QVAC-17990 Add ESRGAN cancel integration coverage
gabrielgrigoras-serv May 7, 2026
eaa5ebe
QVAC-17990 Add ESRGAN cancel and coexistence tests
gabrielgrigoras-serv May 7, 2026
9ecb542
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 7, 2026
ad8acc6
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 7, 2026
65a3fa0
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 7, 2026
4d083df
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 8, 2026
0e39142
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 8, 2026
4bc809f
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 8, 2026
078a9c2
QVAC-17990 Fix C++ lint issues
gabrielgrigoras-serv May 8, 2026
e8ee6bb
QVAC-17990 Sync mobile integration manifest
gabrielgrigoras-serv May 8, 2026
1216b52
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gabrielgrigoras-serv May 8, 2026
5c5540c
QVAC-17990 Use global native logging setup
gabrielgrigoras-serv May 8, 2026
b1fa53d
QVAC-17990 Document standalone EsrganUpscaler API
gianni-cor May 8, 2026
9e71b52
QVAC-17990 Fix standalone ESRGAN example lint
gabrielgrigoras-serv May 8, 2026
f439556
Merge branch 'main' into feat/QVAC-17990-standalone-esrgan-upscale
gianni-cor May 8, 2026
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
11 changes: 11 additions & 0 deletions packages/diffusion-cpp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [0.7.0] - 2026-05-06

### Added

- Standalone ESRGAN upscaler API via named export `EsrganUpscaler` for upscaling existing PNG/JPEG images without loading a diffusion model
- End-to-end ESRGAN integration coverage for both post-generation upscale and standalone upscale output dimensions

### Changed

- Native log routing is no longer connected/released per instance; configure process-global native C++ logs through `addonLogging.setLogger()` for coexistence safety

## [0.6.0] - 2026-05-01

### Added
Expand Down
5 changes: 4 additions & 1 deletion packages/diffusion-cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ add_bare_module(qvac-lib-inference-addon-sd EXPORTS ${BACKEND_DL_EXPORTS})
${PROJECT_SOURCE_DIR}/addon/src/js-interface/binding.cpp
${PROJECT_SOURCE_DIR}/addon/src/handlers/SdCtxHandlers.cpp
${PROJECT_SOURCE_DIR}/addon/src/handlers/SdGenHandlers.cpp
${PROJECT_SOURCE_DIR}/addon/src/model-interface/EsrganUpscalerModel.cpp
${PROJECT_SOURCE_DIR}/addon/src/model-interface/SdModel.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/BackendLoader.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/EsrganUpscaler.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/ImageCodec.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/ImageUtils.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/LoggingMacros.cpp
${PROJECT_SOURCE_DIR}/addon/src/utils/BackendSelection.cpp
Expand Down Expand Up @@ -143,4 +147,3 @@ if(WIN32)
msvcrt.lib
)
endif()

70 changes: 68 additions & 2 deletions packages/diffusion-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Native C++ addon for text-to-image generation using [qvac-ext-stable-diffusion.c
- [Image-to-image (`init_image`)](#image-to-image-init_image)
- [Multi-reference fusion (`init_images`) — FLUX.2 only](#multi-reference-fusion-init_images--flux2-only)
- [7. Release Resources](#7-release-resources)
- [Standalone ESRGAN Upscaler](#standalone-esrgan-upscaler)
- [Model File Reference](#model-file-reference)
- [FLUX.2 \[klein\] 4B (recommended for 16 GB machines)](#flux2-klein-4b-recommended-for-16-gb-machines)
- [Stable Diffusion 1.x / 2.x](#stable-diffusion-1x--2x)
Expand Down Expand Up @@ -172,6 +173,7 @@ Source: [`examples/generate-image.js`](./examples/generate-image.js)
- [Generate Image (SD3)](./examples/generate-image-sd3.js) – Text-to-image with SD3 Medium (safetensors, diffusion + CLIP encoders).
- [Generate Image (SDXL)](./examples/generate-image-sdxl.js) – Text-to-image with an SDXL base all-in-one GGUF model.
- [Post-generation ESRGAN Upscale](./examples/generate-image-esrgan-upscale.js) – Text-to-image with SD2.1 followed by one or two ESRGAN upscale passes.
- [Standalone ESRGAN Upscale](./examples/standalone-esrgan-upscale.js) – Upscale an existing PNG/JPEG without loading a diffusion model (named export `EsrganUpscaler`).
- [Runtime Stats](./examples/runtime-stats-sd2.js) – Run SD2.1 inference and report runtime statistics.
- [img2img FLUX2](./examples/img2img-flux2.js) – Transform an image with FLUX2-klein (Q8_0, in-context conditioning).
- [img2img FLUX2 F16](./examples/img2img-flux2-f16.js) – Transform an image with FLUX2-klein (F16 full precision).
Expand Down Expand Up @@ -216,9 +218,11 @@ const args = {
| `files.vae` | — | Absolute path to separate VAE file |
| `files.esrgan` | — | Absolute path to ESRGAN upscaler model for post-generation upscale |
| `config` | — | Native backend configuration object (see next section) |
| `logger` | — | Logger instance (e.g. `console`) |
| `logger` | — | Logger instance for JS wrapper logs (e.g. `console`) |
| `opts` | — | Additional options (e.g. `{ stats: true }`) |

Native C++ logs are process-global. Configure native log routing once with `require('@qvac/diffusion-cpp/addonLogging').setLogger(...)`.

### 3. Configure the native backend (`args.config`)

`config` is a field on the `args` object built in step 2 — there is no separate constructor argument. The native backend reads it during `load()`.
Expand Down Expand Up @@ -437,6 +441,68 @@ await model.unload()

---

## Standalone ESRGAN Upscaler

The package also exports `EsrganUpscaler` for upscaling existing PNG/JPEG images
without loading a diffusion model. Useful for post-processing pre-existing assets
(screenshots, photos, third-party generated images).

```js
const { EsrganUpscaler } = require('@qvac/diffusion-cpp')
const { setLogger, releaseLogger } = require('@qvac/diffusion-cpp/addonLogging')
const fs = require('bare-fs')

setLogger((priority, message) => console.log(`[C++] ${message}`))

const upscaler = new EsrganUpscaler({
files: {
esrgan: '/absolute/path/to/RealESRGAN_x4plus_anime_6B.pth'
},
config: {
upscaler_tile_size: 128
},
logger: console
})

await upscaler.load()

const inputBytes = fs.readFileSync('/path/to/source.png')
const response = await upscaler.upscale(inputBytes, { repeats: 1 })

const images = []
await response
.onUpdate(data => {
if (data instanceof Uint8Array) images.push(data)
})
.await()

await upscaler.unload()
releaseLogger()
```

**Constructor args**

| Property | Required | Description |
|----------|----------|-------------|
| `files.esrgan` | ✅ | Absolute path to ESRGAN upscaler model (`.pth`) |
| `config.upscaler_tile_size` | — | Tile size used during inference (default `128`) |
| `config.upscaler_threads` | — | CPU threads for the upscaler (`-1` = auto) |
| `config.upscaler_direct` | — | Use direct convolution (default `false`) |
| `config.upscaler_offload_params_to_cpu` | — | Keep weights on CPU and offload during compute (default `false`) |
| `logger` | — | Logger instance for JS wrapper logs (e.g. `console`). Native C++ logs are configured separately via `addonLogging.setLogger()` |

**`upscale(imageBytes, options?)`**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `repeats` | number | `1` | Number of ESRGAN passes. Each pass multiplies output dimensions by the model's scale factor (typically 4×), so `repeats: 2` produces a 16× upscale |

Cancellation works the same as `ImgStableDiffusion`: call `upscaler.cancel()` to interrupt an in-flight upscale (honored between repeat passes).

For a complete runnable example, see [`examples/standalone-esrgan-upscale.js`](./examples/standalone-esrgan-upscale.js).

---

## Model File Reference

### FLUX.2 [klein] 4B (recommended for 16 GB machines)
Expand Down Expand Up @@ -592,4 +658,4 @@ must be released under a compatible CC BY-SA license.

## License

Apache-2.0 — see [LICENSE](./LICENSE) for details.
Apache-2.0 — see [LICENSE](./LICENSE) for details.
63 changes: 62 additions & 1 deletion packages/diffusion-cpp/addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,65 @@ class SdInterface {
}
}

module.exports = { SdInterface, mapAddonEvent, readImageDimensions }
class EsrganUpscalerInterface {
/**
* @param {object} binding - The native addon binding (from require.addon())
* @param {object} configurationParams - Configuration for the ESRGAN context
* @param {string} configurationParams.esrganPath - Local file path to ESRGAN weights
* @param {object} [configurationParams.config] - ESRGAN-specific configuration options
* @param {Function} outputCb - Called on any upscale event
*/
constructor (binding, configurationParams, outputCb) {
this._binding = binding

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

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

configurationParams.config = Object.fromEntries(
Object.entries(configurationParams.config)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
)

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

async activate () {
this._binding.activateUpscaler(this._handle)
}

async cancel () {
if (!this._handle) return
await this._binding.cancel(this._handle)
}

async runJob (imageBytes, params) {
return this._binding.runUpscaleJob(this._handle, {
type: 'image',
input: imageBytes,
params: JSON.stringify(params || {})
})
}

async unload () {
if (!this._handle) return
this._binding.destroyInstance(this._handle)
this._handle = null
}
}

module.exports = {
SdInterface,
EsrganUpscalerInterface,
mapAddonEvent,
readImageDimensions
}
126 changes: 126 additions & 0 deletions packages/diffusion-cpp/addon/src/addon/AddonJs.hpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
#pragma once

#include <cmath>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include <picojson/picojson.h>
#include <qvac-lib-inference-addon-cpp/JsInterface.hpp>
#include <qvac-lib-inference-addon-cpp/JsUtils.hpp>
#include <qvac-lib-inference-addon-cpp/ModelInterfaces.hpp>
Expand All @@ -12,10 +17,47 @@
#include <qvac-lib-inference-addon-cpp/queue/OutputCallbackJs.hpp>

#include "handlers/SdCtxHandlers.hpp"
#include "model-interface/EsrganUpscalerModel.hpp"
#include "model-interface/SdModel.hpp"

namespace qvac_lib_inference_addon_sd {

inline int parseStandaloneUpscaleRepeats(const std::string& paramsJson) {
picojson::value v;
const std::string parseErr = picojson::parse(v, paramsJson);
if (!parseErr.empty()) {
throw qvac_errors::StatusError(
qvac_errors::general_error::InvalidArgument,
"Failed to parse ESRGAN upscale params JSON: " + parseErr);
}
if (!v.is<picojson::object>()) {
throw qvac_errors::StatusError(
qvac_errors::general_error::InvalidArgument,
"ESRGAN upscale params must be a JSON object");
}

const auto& obj = v.get<picojson::object>();
auto it = obj.find("repeats");
if (it == obj.end() || it->second.is<picojson::null>()) {
return 1;
}
if (!it->second.is<double>()) {
throw qvac_errors::StatusError(
qvac_errors::general_error::InvalidArgument,
"upscale.repeats must be a positive integer");
}

const double raw = it->second.get<double>();
if (!std::isfinite(raw) || raw <= 0 || std::floor(raw) != raw ||
raw > static_cast<double>(std::numeric_limits<int>::max())) {
throw qvac_errors::StatusError(
qvac_errors::general_error::InvalidArgument,
"upscale.repeats must be a positive integer");
}

return static_cast<int>(raw);
}

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;
Expand Down Expand Up @@ -68,6 +110,36 @@ inline js_value_t* createInstance(js_env_t* env, js_callback_info_t* info) try {
}
JSCATCH

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

JsArgsParser args(env, info);

SdCtxConfig config{};
config.esrganPath = args.getMapEntry(1, "esrganPath");

auto configMap = args.getSubmap(1, "config");
applySdCtxHandlers(config, configMap);

auto model = make_unique<EsrganUpscalerModel>(std::move(config));

out_handl::OutputHandlers<out_handl::JsOutputHandlerInterface> outHandlers;
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;
Expand Down Expand Up @@ -125,6 +197,37 @@ inline js_value_t* runJob(js_env_t* env, js_callback_info_t* info) try {
}
JSCATCH

inline js_value_t* runUpscaleJob(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 != "image") {
throw StatusError(
general_error::InvalidArgument,
"ESRGAN runUpscaleJob expects a single image input");
}

auto inputObj = args.getJsObject(1, "inputObj");
const string paramsJson =
inputObj.getOptionalPropertyAs<js::String, std::string>(env, "params")
.value_or("{}");

EsrganUpscalerModel::UpscaleJob job;
job.imageBytes =
js::TypedArray<uint8_t>(env, jsInput).as<std::vector<uint8_t>>(env);
job.repeats = parseStandaloneUpscaleRepeats(paramsJson);
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

/**
* Activate the addon -- loads model weights by calling SdModel::load()
* directly. SdModel does not implement IModelAsyncLoad, so we bypass
Expand All @@ -151,4 +254,27 @@ inline js_value_t* activate(js_env_t* env, js_callback_info_t* info) try {
}
JSCATCH

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

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

auto* upscalerModel =
dynamic_cast<EsrganUpscalerModel*>(&instance.addonCpp->model.get());
if (upscalerModel == nullptr) {
throw StatusError(
general_error::InternalError,
"activateUpscaler: model is not an EsrganUpscalerModel");
}

upscalerModel->load();

js_value_t* result = nullptr;
js_get_undefined(env, &result);
return result;
}
JSCATCH

} // namespace qvac_lib_inference_addon_sd
Loading
Loading