From 64912200a722e615d805e73bf2af07b8c011f407 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot Date: Sun, 25 Oct 2026 21:23:13 +0100 Subject: [PATCH] chore: :arrow_up: Update leejet/stable-diffusion.cpp to `a85db96aa5ce2c72bd5aec326f6c5c857839a50a` (#12845) * :arrow_up: Update leejet/stable-diffusion.cpp Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(sd): adopt owning generation results The sd.cpp bump replaces raw image and video outputs with owning results. Adapt image generation, video generation and upscaling to the new API. Use upstream destructors to release all pixels, audio and metadata. Add model-free native checks for result ownership and failure paths. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: Ettore Di Giacinto Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> Co-authored-by: Ettore Di Giacinto --- backend/go/stablediffusion-ggml/Makefile | 2 +- backend/go/stablediffusion-ggml/cpp/gosd.cpp | 70 +++++----- .../go/stablediffusion-ggml/tests/README.md | 30 +++++ .../stablediffusion-ggml/tests/result_api.cpp | 122 ++++++++++++++++++ .../stablediffusion-ggml/tests/result_api.sh | 17 +++ 5 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 backend/go/stablediffusion-ggml/tests/README.md create mode 100644 backend/go/stablediffusion-ggml/tests/result_api.cpp create mode 100644 backend/go/stablediffusion-ggml/tests/result_api.sh diff --git a/backend/go/stablediffusion-ggml/Makefile b/backend/go/stablediffusion-ggml/Makefile index 3c8414b34..98950a671 100644 --- a/backend/go/stablediffusion-ggml/Makefile +++ b/backend/go/stablediffusion-ggml/Makefile @@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1) # stablediffusion.cpp (ggml) STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp -STABLEDIFFUSION_GGML_VERSION?=a77d3623180b2bc1886f3dd04c0aa04156a987ef +STABLEDIFFUSION_GGML_VERSION?=a85db96aa5ce2c72bd5aec326f6c5c857839a50a CMAKE_ARGS+=-DGGML_MAX_NAME=128 diff --git a/backend/go/stablediffusion-ggml/cpp/gosd.cpp b/backend/go/stablediffusion-ggml/cpp/gosd.cpp index d2fd95e1b..f22285bef 100644 --- a/backend/go/stablediffusion-ggml/cpp/gosd.cpp +++ b/backend/go/stablediffusion-ggml/cpp/gosd.cpp @@ -814,8 +814,7 @@ void sd_img_gen_params_set_custom_sigmas(sd_img_gen_params_t *params, float *sig int gen_image(sd_img_gen_params_t *p, int steps, char *dst, float cfg_scale, char *src_image, float strength, char *mask_image, char* ref_images[], int ref_images_count) { - sd_image_t* results; - int num_results_out = 0; + sd_image_generation_result_t* generated = nullptr; std::vector skip_layers = {7, 8, 9}; @@ -1012,15 +1011,13 @@ int gen_image(sd_img_gen_params_t *p, int steps, char *dst, float cfg_scale, cha sd_ctx_params_to_str(&ctx_params), sd_img_gen_params_to_str(p)); - bool gen_ok = generate_image(sd_c, p, &results, &num_results_out); + bool gen_ok = generate_image(sd_c, p, &generated); + std::unique_ptr + output(generated, free_sd_image_generation_result); std::free(p); - if (!gen_ok || num_results_out == 0) { - results = NULL; - } - - if (results == NULL) { + if (!gen_ok || !output || output->count <= 0 || !output->items) { fprintf (stderr, "NO results\n"); if (input_image_buffer) free(input_image_buffer); if (mask_image_buffer) free(mask_image_buffer); @@ -1030,7 +1027,8 @@ int gen_image(sd_img_gen_params_t *p, int steps, char *dst, float cfg_scale, cha return 1; } - if (results[0].data == NULL) { + const sd_image_t& result = output->items[0].image; + if (result.data == NULL) { fprintf (stderr, "Results with no data\n"); if (input_image_buffer) free(input_image_buffer); if (mask_image_buffer) free(mask_image_buffer); @@ -1043,22 +1041,19 @@ int gen_image(sd_img_gen_params_t *p, int steps, char *dst, float cfg_scale, cha fprintf (stderr, "Writing PNG\n"); fprintf (stderr, "DST: %s\n", dst); - fprintf (stderr, "Width: %d\n", results[0].width); - fprintf (stderr, "Height: %d\n", results[0].height); - fprintf (stderr, "Channel: %d\n", results[0].channel); - fprintf (stderr, "Data: %p\n", results[0].data); + fprintf (stderr, "Width: %d\n", result.width); + fprintf (stderr, "Height: %d\n", result.height); + fprintf (stderr, "Channel: %d\n", result.channel); + fprintf (stderr, "Data: %p\n", result.data); - int ret = stbi_write_png(dst, results[0].width, results[0].height, results[0].channel, - results[0].data, 0); + int ret = stbi_write_png(dst, result.width, result.height, result.channel, + result.data, 0); if (ret) fprintf (stderr, "Saved resulting image to '%s'\n", dst); else fprintf(stderr, "Failed to write image to '%s'\n", dst); // Clean up - free(results[0].data); - results[0].data = NULL; - free(results); if (input_image_buffer) free(input_image_buffer); if (mask_image_buffer) free(mask_image_buffer); for (auto buffer : ref_image_buffers) { @@ -1590,29 +1585,22 @@ int gen_video(sd_vid_gen_params_t *p, int steps, char *dst, float cfg_scale, int if (!ref_auds.empty()) { p->ref_audios = ref_auds.data(); p->ref_audios_count = (int)ref_auds.size(); } // Generate - int num_frames_out = 0; - int effective_fps = fps; - sd_image_t* frames = nullptr; - sd_audio_t* audio = nullptr; - bool ok = generate_video(sd_c, p, &frames, &num_frames_out, &audio, &effective_fps); + sd_video_generation_result_t* generated = nullptr; + bool ok = generate_video(sd_c, p, &generated); + std::unique_ptr + output(generated, free_sd_video_generation_result); std::free(p); - if (!ok || !frames || num_frames_out == 0) { + if (!ok || !output || !output->frames || output->frame_count <= 0) { fprintf(stderr, "generate_video produced no frames\n"); - if (audio) free_sd_audio(audio); free_inputs(); return 1; } - fprintf(stderr, "Generated %d frames, muxing to %s via ffmpeg\n", num_frames_out, dst); + fprintf(stderr, "Generated %d frames, muxing to %s via ffmpeg\n", output->frame_count, dst); - int rc = ffmpeg_mux_raw_to_mp4(frames, num_frames_out, effective_fps, audio, dst); - - for (int i = 0; i < num_frames_out; i++) { - if (frames[i].data) free(frames[i].data); - } - free(frames); - if (audio) free_sd_audio(audio); + int rc = ffmpeg_mux_raw_to_mp4(output->frames, output->frame_count, output->fps, + output->audio, dst); free_inputs(); if (rc == 0) { @@ -1665,9 +1653,8 @@ int gosd_upscaler_run(void* handle, const char* src, const char* dst, int scale) if (!handle || !src || !*src || !dst || !*dst || scale <= 0) return 1; // RAII also covers exceptions from native inference. struct images { - sd_image_t* data = nullptr; - int count = 0; - ~images() { if (data) free_sd_images(data, count); } + sd_image_generation_result_t* data = nullptr; + ~images() { free_sd_image_generation_result(data); } } output; int width = 0, height = 0, channels = 0; std::unique_ptr input( @@ -1680,12 +1667,13 @@ int gosd_upscaler_run(void* handle, const char* src, const char* dst, int scale) sd_image_t image = {uint32_t(width), uint32_t(height), 3, input.get()}; try { if (!upscale(static_cast(handle)->ctx, image, scale, - &output.data, &output.count)) return 3; - if (!output.data || output.count != 1 || !output.data[0].data || - output.data[0].width != out_width || output.data[0].height != out_height || - output.data[0].channel != 3) return 4; + &output.data)) return 3; + if (!output.data || output.data->count != 1 || !output.data->items) return 4; + const sd_image_t& result = output.data->items[0].image; + if (!result.data || result.width != out_width || result.height != out_height || + result.channel != 3) return 4; if (!stbi_write_png(dst, int(out_width), int(out_height), 3, - output.data[0].data, int(out_width) * 3)) return 5; + result.data, int(out_width) * 3)) return 5; return 0; } catch (...) { return 3; } } diff --git a/backend/go/stablediffusion-ggml/tests/README.md b/backend/go/stablediffusion-ggml/tests/README.md new file mode 100644 index 000000000..60f062f14 --- /dev/null +++ b/backend/go/stablediffusion-ggml/tests/README.md @@ -0,0 +1,30 @@ +# Native result API regression checks + +Run from the backend directory after fetching the pinned upstream sources: + +```sh +sh tests/result_api.sh +``` + +Alternatively, pass an existing stable-diffusion.cpp checkout: + +```sh +sh tests/result_api.sh /path/to/stable-diffusion.cpp +``` + +The test compiles the binding against the upstream header and stubs inference; +no models or native libraries are required. It checks image batch cleanup, +empty/failed generation, missing pixels, PNG write failures, video result cleanup +(including audio and metadata), and upscaler validation and exception cleanup. +It does not run successful video encoding or inference. The linker discards +unused binding functions so only the exercised native API calls need stubs. + +To check memory ownership with AddressSanitizer and UndefinedBehaviorSanitizer: + +```sh +CXXFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g' \ +LDFLAGS='-fsanitize=address,undefined' sh tests/result_api.sh +``` + +Set `TMPDIR` to choose where the runner creates its temporary files. `CXX` +selects the compiler. Do not pass `-DNDEBUG`: the checks use assertions. diff --git a/backend/go/stablediffusion-ggml/tests/result_api.cpp b/backend/go/stablediffusion-ggml/tests/result_api.cpp new file mode 100644 index 000000000..e81d3dcd0 --- /dev/null +++ b/backend/go/stablediffusion-ggml/tests/result_api.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +// Model-free binding checks against the pinned upstream header. Linker section +// GC lets the test stub only the native calls used by these entry points. +#include "../cpp/gosd.cpp" +#include +#include + +static int mode = 0; +static int image_frees = 0; +static int video_frees = 0; + +static sd_image_generation_result_t* image_result(int width, int height, int count) { + auto* result = new sd_image_generation_result_t{}; + result->count = count; + result->items = new sd_image_result_t[count]{}; + for (int i = 0; i < count; ++i) { + result->items[i].image = {uint32_t(width), uint32_t(height), 3, + new uint8_t[width * height * 3]{}}; + result->items[i].generation_info = new char[3]{'{', '}', 0}; + } + return result; +} + +void free_sd_image_generation_result(sd_image_generation_result_t* result) { + if (!result) return; + ++image_frees; + for (int i = 0; i < result->count; ++i) { + delete[] result->items[i].image.data; + delete[] result->items[i].generation_info; + } + delete[] result->items; + delete result; +} + +void free_sd_video_generation_result(sd_video_generation_result_t* result) { + if (!result) return; + ++video_frees; + for (int i = 0; i < result->frame_count; ++i) delete[] result->frames[i].data; + delete[] result->frames; + if (result->audio) { + delete[] result->audio->data; + delete result->audio; + } + delete[] result->generation_info; + delete result; +} + +char* sd_ctx_params_to_str(const sd_ctx_params_t*) { return const_cast("test"); } +char* sd_img_gen_params_to_str(const sd_img_gen_params_t*) { return const_cast("test"); } +void free_upscaler_ctx(upscaler_ctx_t*) {} + +bool generate_image(sd_ctx_t*, const sd_img_gen_params_t*, sd_image_generation_result_t** out) { + *out = nullptr; + if (mode == 1) return false; + *out = image_result(2, 2, mode == 2 ? 0 : 2); + if (mode == 3) { + delete[] (*out)->items[0].image.data; + (*out)->items[0].image.data = nullptr; + } + return true; +} + +bool generate_video(sd_ctx_t*, const sd_vid_gen_params_t*, sd_video_generation_result_t** out) { + *out = nullptr; + if (mode == 1) return false; + *out = new sd_video_generation_result_t{}; + (*out)->fps = 12; + (*out)->audio = new sd_audio_t{16000, 1, 1, new float[1]{}}; + (*out)->generation_info = new char[3]{'{', '}', 0}; + // A frame without pixels makes muxing fail before launching ffmpeg. + if (mode == 3) { + (*out)->frame_count = 1; + (*out)->frames = new sd_image_t[1]{}; + } + return true; +} + +bool upscale(upscaler_ctx_t*, sd_image_t image, uint32_t scale, sd_image_generation_result_t** out) { + *out = nullptr; + if (mode == 1) return false; + *out = image_result(image.width * scale, image.height * scale, mode == 2 ? 0 : 1); + if (mode == 3) (*out)->items[0].image.width = 1; + if (mode == 4) throw std::runtime_error("inference failure after allocation"); + return true; +} + +int main(int argc, char** argv) { + assert(argc == 2); + std::string root = argv[1]; + std::string input = root + "/input.png"; + std::string output = root + "/output.png"; + std::string missing = root + "/missing/output.png"; + uint8_t pixels[12]{}; + assert(stbi_write_png(input.c_str(), 2, 2, 3, pixels, 0)); + + for (mode = 0; mode <= 4; ++mode) { + auto* params = static_cast(calloc(1, sizeof(sd_img_gen_params_t))); + params->width = params->height = 2; + int before = image_frees; + int rc = gen_image(params, 1, const_cast((mode == 4 ? missing : output).c_str()), + 1, nullptr, 0, nullptr, nullptr, 0); + assert((rc == 0) == (mode == 0)); + assert(image_frees - before == (mode == 1 ? 0 : 1)); + } + for (mode = 1; mode <= 3; ++mode) { + auto* params = static_cast(calloc(1, sizeof(sd_vid_gen_params_t))); + int before = video_frees; + assert(gen_video(params, 1, const_cast(output.c_str()), 1, 24, + nullptr, nullptr, nullptr, nullptr, nullptr) != 0); + assert(video_frees - before == (mode == 1 ? 0 : 1)); + } + gosd_upscaler handle; + for (mode = 0; mode <= 5; ++mode) { + int before = image_frees; + int rc = gosd_upscaler_run(&handle, input.c_str(), + (mode == 5 ? missing : output).c_str(), 2); + const int expected[] = {0, 3, 4, 4, 3, 5}; + assert(rc == expected[mode]); + assert(image_frees - before == (mode == 1 ? 0 : 1)); + } + puts("result API binding checks passed"); +} diff --git a/backend/go/stablediffusion-ggml/tests/result_api.sh b/backend/go/stablediffusion-ggml/tests/result_api.sh new file mode 100644 index 000000000..6f3917065 --- /dev/null +++ b/backend/go/stablediffusion-ggml/tests/result_api.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# Usage: sh tests/result_api.sh [path-to-stable-diffusion.cpp] +set -eu +backend=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +upstream=${1:-"$backend/sources/stablediffusion-ggml.cpp"} +work=$(mktemp -d "${TMPDIR:-/tmp}/gosd-result-api.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +case $(uname -s) in + Darwin) gc=-Wl,-dead_strip ;; + *) gc=-Wl,--gc-sections ;; +esac +# CXXFLAGS/LDFLAGS may enable sanitizers; do not use -DNDEBUG (assertions test behavior). +${CXX:-c++} ${CXXFLAGS:-} -std=c++17 -ffunction-sections -fdata-sections \ + -I"$upstream/include" -I"$upstream/thirdparty" \ + "$backend/tests/result_api.cpp" "$gc" ${LDFLAGS:-} -o "$work/result-api" +"$work/result-api" "$work"