diff --git a/backend/go/stablediffusion-ggml/Makefile b/backend/go/stablediffusion-ggml/Makefile index 3c8414b34c7d..98950a671719 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 d2fd95e1b51c..f22285bef153 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 000000000000..60f062f14dfd --- /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 000000000000..e81d3dcd0436 --- /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 000000000000..6f3917065ccd --- /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"