From 96c3e64057b0954cfcfeec8ad301934241e50c65 Mon Sep 17 00:00:00 2001 From: leejet Date: Mon, 8 Dec 2025 23:59:04 +0800 Subject: [PATCH 01/49] refactor: optimize the handling of embedding (#1068) * optimize the handling of embedding * support case-insensitive embedding names --- clip.hpp | 78 +++++++++++++++++++++++++++++-------- conditioner.hpp | 90 +++++++++++++++---------------------------- examples/cli/main.cpp | 61 ++++++++++++++++++++++++++++- stable-diffusion.cpp | 10 +++-- stable-diffusion.h | 8 +++- 5 files changed, 165 insertions(+), 82 deletions(-) diff --git a/clip.hpp b/clip.hpp index 1f983271f..0070833cb 100644 --- a/clip.hpp +++ b/clip.hpp @@ -3,6 +3,7 @@ #include "ggml_extend.hpp" #include "model.h" +#include "tokenize_util.h" /*================================================== CLIPTokenizer ===================================================*/ @@ -72,6 +73,8 @@ class CLIPTokenizer { int encoder_len; int bpe_len; + std::vector special_tokens; + public: const std::string UNK_TOKEN = "<|endoftext|>"; const std::string BOS_TOKEN = "<|startoftext|>"; @@ -117,6 +120,15 @@ class CLIPTokenizer { return pairs; } + bool is_special_token(const std::string& token) { + for (auto& special_token : special_tokens) { + if (special_token == token) { + return true; + } + } + return false; + } + public: CLIPTokenizer(int pad_token_id = 49407, const std::string& merges_utf8_str = "") : PAD_TOKEN_ID(pad_token_id) { @@ -125,6 +137,8 @@ class CLIPTokenizer { } else { load_from_merges(ModelLoader::load_merges()); } + add_special_token("<|startoftext|>"); + add_special_token("<|endoftext|>"); } void load_from_merges(const std::string& merges_utf8_str) { @@ -201,6 +215,10 @@ class CLIPTokenizer { } } + void add_special_token(const std::string& token) { + special_tokens.push_back(token); + } + std::u32string bpe(const std::u32string& token) { std::vector word; @@ -379,25 +397,54 @@ class CLIPTokenizer { return trim(text); } + std::vector token_split(const std::string& text) { + std::regex pat(R"('s|'t|'re|'ve|'m|'ll|'d|[[:alpha:]]+|[[:digit:]]|[^[:space:][:alpha:][:digit:]]+)", + std::regex::icase); + std::sregex_iterator iter(text.begin(), text.end(), pat); + std::sregex_iterator end; + + std::vector result; + for (; iter != end; ++iter) { + result.emplace_back(iter->str()); + } + + return result; + } + std::vector encode(std::string text, on_new_token_cb_t on_new_token_cb) { std::string original_text = text; std::vector bpe_tokens; text = whitespace_clean(text); std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return std::tolower(c); }); - std::regex pat(R"(<\|startoftext\|>|<\|endoftext\|>|'s|'t|'re|'ve|'m|'ll|'d|[[:alpha:]]+|[[:digit:]]|[^[:space:][:alpha:][:digit:]]+)", - std::regex::icase); - - std::smatch matches; std::string str = text; std::vector token_strs; - while (std::regex_search(str, matches, pat)) { - bool skip = on_new_token_cb(str, bpe_tokens); - if (skip) { + + auto splited_texts = split_with_special_tokens(text, special_tokens); + + for (auto& splited_text : splited_texts) { + LOG_DEBUG("token %s", splited_text.c_str()); + if (is_special_token(splited_text)) { + LOG_DEBUG("special %s", splited_text.c_str()); + bool skip = on_new_token_cb(splited_text, bpe_tokens); + if (skip) { + token_strs.push_back(splited_text); + continue; + } continue; } - for (auto& token : matches) { - std::string token_str = token.str(); + + auto tokens = token_split(splited_text); + for (auto& token : tokens) { + if (on_new_token_cb != nullptr) { + bool skip = on_new_token_cb(token, bpe_tokens); + if (skip) { + token_strs.push_back(token); + continue; + } + } + + std::string token_str = token; std::u32string utf32_token; for (int i = 0; i < token_str.length(); i++) { unsigned char b = token_str[i]; @@ -417,14 +464,13 @@ class CLIPTokenizer { bpe_tokens.push_back(encoder[bpe_str]); token_strs.push_back(utf32_to_utf8(bpe_str)); } - str = matches.suffix(); - } - std::stringstream ss; - ss << "["; - for (auto token : token_strs) { - ss << "\"" << token << "\", "; } - ss << "]"; + // std::stringstream ss; + // ss << "["; + // for (auto token : token_strs) { + // ss << "\"" << token << "\", "; + // } + // ss << "]"; // LOG_DEBUG("split prompt \"%s\" to tokens %s", original_text.c_str(), ss.str().c_str()); // printf("split prompt \"%s\" to tokens %s \n", original_text.c_str(), ss.str().c_str()); return bpe_tokens; diff --git a/conditioner.hpp b/conditioner.hpp index 2e5972c1b..55e1502e8 100644 --- a/conditioner.hpp +++ b/conditioner.hpp @@ -56,7 +56,7 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { std::shared_ptr text_model2; std::string trigger_word = "img"; // should be user settable - std::string embd_dir; + std::map embedding_map; int32_t num_custom_embeddings = 0; int32_t num_custom_embeddings_2 = 0; std::vector token_embed_custom; @@ -65,11 +65,17 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { FrozenCLIPEmbedderWithCustomWords(ggml_backend_t backend, bool offload_params_to_cpu, const String2TensorStorage& tensor_storage_map, - const std::string& embd_dir, + const std::map& orig_embedding_map, SDVersion version = VERSION_SD1, PMVersion pv = PM_VERSION_1) - : version(version), pm_version(pv), tokenizer(sd_version_is_sd2(version) ? 0 : 49407), embd_dir(embd_dir) { - bool force_clip_f32 = embd_dir.size() > 0; + : version(version), pm_version(pv), tokenizer(sd_version_is_sd2(version) ? 0 : 49407) { + for (const auto& kv : orig_embedding_map) { + std::string name = kv.first; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); }); + embedding_map[name] = kv.second; + tokenizer.add_special_token(name); + } + bool force_clip_f32 = !embedding_map.empty(); if (sd_version_is_sd1(version)) { text_model = std::make_shared(backend, offload_params_to_cpu, tensor_storage_map, "cond_stage_model.transformer.text_model", OPENAI_CLIP_VIT_L_14, true, force_clip_f32); } else if (sd_version_is_sd2(version)) { @@ -196,25 +202,13 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { std::vector convert_token_to_id(std::string text) { auto on_new_token_cb = [&](std::string& str, std::vector& bpe_tokens) -> bool { - size_t word_end = str.find(","); - std::string embd_name = word_end == std::string::npos ? str : str.substr(0, word_end); - embd_name = trim(embd_name); - std::string embd_path = get_full_path(embd_dir, embd_name + ".pt"); - if (embd_path.size() == 0) { - embd_path = get_full_path(embd_dir, embd_name + ".ckpt"); + auto iter = embedding_map.find(str); + if (iter == embedding_map.end()) { + return false; } - if (embd_path.size() == 0) { - embd_path = get_full_path(embd_dir, embd_name + ".safetensors"); - } - if (embd_path.size() > 0) { - if (load_embedding(embd_name, embd_path, bpe_tokens)) { - if (word_end != std::string::npos) { - str = str.substr(word_end); - } else { - str = ""; - } - return true; - } + std::string embedding_path = iter->second; + if (load_embedding(str, embedding_path, bpe_tokens)) { + return true; } return false; }; @@ -245,25 +239,13 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { } auto on_new_token_cb = [&](std::string& str, std::vector& bpe_tokens) -> bool { - size_t word_end = str.find(","); - std::string embd_name = word_end == std::string::npos ? str : str.substr(0, word_end); - embd_name = trim(embd_name); - std::string embd_path = get_full_path(embd_dir, embd_name + ".pt"); - if (embd_path.size() == 0) { - embd_path = get_full_path(embd_dir, embd_name + ".ckpt"); - } - if (embd_path.size() == 0) { - embd_path = get_full_path(embd_dir, embd_name + ".safetensors"); + auto iter = embedding_map.find(str); + if (iter == embedding_map.end()) { + return false; } - if (embd_path.size() > 0) { - if (load_embedding(embd_name, embd_path, bpe_tokens)) { - if (word_end != std::string::npos) { - str = str.substr(word_end); - } else { - str = ""; - } - return true; - } + std::string embedding_path = iter->second; + if (load_embedding(str, embedding_path, bpe_tokens)) { + return true; } return false; }; @@ -376,25 +358,13 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { } auto on_new_token_cb = [&](std::string& str, std::vector& bpe_tokens) -> bool { - size_t word_end = str.find(","); - std::string embd_name = word_end == std::string::npos ? str : str.substr(0, word_end); - embd_name = trim(embd_name); - std::string embd_path = get_full_path(embd_dir, embd_name + ".pt"); - if (embd_path.size() == 0) { - embd_path = get_full_path(embd_dir, embd_name + ".ckpt"); - } - if (embd_path.size() == 0) { - embd_path = get_full_path(embd_dir, embd_name + ".safetensors"); + auto iter = embedding_map.find(str); + if (iter == embedding_map.end()) { + return false; } - if (embd_path.size() > 0) { - if (load_embedding(embd_name, embd_path, bpe_tokens)) { - if (word_end != std::string::npos) { - str = str.substr(word_end); - } else { - str = ""; - } - return true; - } + std::string embedding_path = iter->second; + if (load_embedding(str, embedding_path, bpe_tokens)) { + return true; } return false; }; @@ -1728,7 +1698,7 @@ struct LLMEmbedder : public Conditioner { std::vector> image_embeds; std::pair prompt_attn_range; int prompt_template_encode_start_idx = 34; - int max_length = 0; + int max_length = 0; std::set out_layers; if (llm->enable_vision && conditioner_params.ref_images.size() > 0) { LOG_INFO("QwenImageEditPlusPipeline"); @@ -1828,7 +1798,7 @@ struct LLMEmbedder : public Conditioner { prompt += "[/INST]"; } else if (version == VERSION_OVIS_IMAGE) { prompt_template_encode_start_idx = 28; - max_length = prompt_template_encode_start_idx + 256; + max_length = prompt_template_encode_start_idx + 256; prompt = "<|im_start|>user\nDescribe the image by detailing the color, quantity, text, shape, size, texture, spatial relationships of the objects and background:"; diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index bf42f5aa4..acb198161 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -501,6 +501,9 @@ struct SDContextParams { std::string tensor_type_rules; std::string lora_model_dir; + std::map embedding_map; + std::vector embedding_array; + rng_type_t rng_type = CUDA_RNG; rng_type_t sampler_rng_type = RNG_TYPE_COUNT; bool offload_params_to_cpu = false; @@ -828,6 +831,37 @@ struct SDContextParams { return options; } + void build_embedding_map() { + static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; + + if (!fs::exists(embedding_dir) || !fs::is_directory(embedding_dir)) { + return; + } + + for (auto& p : fs::directory_iterator(embedding_dir)) { + if (!p.is_regular_file()) + continue; + + auto path = p.path(); + std::string ext = path.extension().string(); + + bool valid = false; + for (auto& e : valid_ext) { + if (ext == e) { + valid = true; + break; + } + } + if (!valid) + continue; + + std::string key = path.stem().string(); + std::string value = path.string(); + + embedding_map[key] = value; + } + } + bool process_and_check(SDMode mode) { if (mode != UPSCALE && model_path.length() == 0 && diffusion_model_path.length() == 0) { fprintf(stderr, "error: the following arguments are required: model_path/diffusion_model\n"); @@ -845,10 +879,24 @@ struct SDContextParams { n_threads = sd_get_num_physical_cores(); } + build_embedding_map(); + return true; } std::string to_string() const { + std::ostringstream emb_ss; + emb_ss << "{\n"; + for (auto it = embedding_map.begin(); it != embedding_map.end(); ++it) { + emb_ss << " \"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != embedding_map.end()) { + emb_ss << ","; + } + emb_ss << "\n"; + } + emb_ss << " }"; + + std::string embeddings_str = emb_ss.str(); std::ostringstream oss; oss << "SDContextParams {\n" << " n_threads: " << n_threads << ",\n" @@ -866,6 +914,7 @@ struct SDContextParams { << " esrgan_path: \"" << esrgan_path << "\",\n" << " control_net_path: \"" << control_net_path << "\",\n" << " embedding_dir: \"" << embedding_dir << "\",\n" + << " embeddings: " << embeddings_str << "\n" << " wtype: " << sd_type_name(wtype) << ",\n" << " tensor_type_rules: \"" << tensor_type_rules << "\",\n" << " lora_model_dir: \"" << lora_model_dir << "\",\n" @@ -898,6 +947,15 @@ struct SDContextParams { } sd_ctx_params_t to_sd_ctx_params_t(bool vae_decode_only, bool free_params_immediately, bool taesd_preview) { + embedding_array.clear(); + embedding_array.reserve(embedding_map.size()); + for (const auto& kv : embedding_map) { + sd_embedding_t item; + item.name = kv.first.c_str(); + item.path = kv.second.c_str(); + embedding_array.emplace_back(item); + } + sd_ctx_params_t sd_ctx_params = { model_path.c_str(), clip_l_path.c_str(), @@ -912,7 +970,8 @@ struct SDContextParams { taesd_path.c_str(), control_net_path.c_str(), lora_model_dir.c_str(), - embedding_dir.c_str(), + embedding_array.data(), + static_cast(embedding_array.size()), photo_maker_path.c_str(), tensor_type_rules.c_str(), vae_decode_only, diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 98c0c84de..9b054244f 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -508,18 +508,22 @@ class StableDiffusionGGML { "model.diffusion_model", version); } else { // SD1.x SD2.x SDXL + std::map embbeding_map; + for (int i = 0; i < sd_ctx_params->embedding_count; i++) { + embbeding_map.emplace(SAFE_STR(sd_ctx_params->embeddings[i].name), SAFE_STR(sd_ctx_params->embeddings[i].path)); + } if (strstr(SAFE_STR(sd_ctx_params->photo_maker_path), "v2")) { cond_stage_model = std::make_shared(clip_backend, offload_params_to_cpu, tensor_storage_map, - SAFE_STR(sd_ctx_params->embedding_dir), + embbeding_map, version, PM_VERSION_2); } else { cond_stage_model = std::make_shared(clip_backend, offload_params_to_cpu, tensor_storage_map, - SAFE_STR(sd_ctx_params->embedding_dir), + embbeding_map, version); } diffusion_model = std::make_shared(backend, @@ -2521,7 +2525,6 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) { "taesd_path: %s\n" "control_net_path: %s\n" "lora_model_dir: %s\n" - "embedding_dir: %s\n" "photo_maker_path: %s\n" "tensor_type_rules: %s\n" "vae_decode_only: %s\n" @@ -2552,7 +2555,6 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) { SAFE_STR(sd_ctx_params->taesd_path), SAFE_STR(sd_ctx_params->control_net_path), SAFE_STR(sd_ctx_params->lora_model_dir), - SAFE_STR(sd_ctx_params->embedding_dir), SAFE_STR(sd_ctx_params->photo_maker_path), SAFE_STR(sd_ctx_params->tensor_type_rules), BOOL_STR(sd_ctx_params->vae_decode_only), diff --git a/stable-diffusion.h b/stable-diffusion.h index e34cdec17..7c72c70a7 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -150,6 +150,11 @@ typedef struct { float rel_size_y; } sd_tiling_params_t; +typedef struct { + const char* name; + const char* path; +} sd_embedding_t; + typedef struct { const char* model_path; const char* clip_l_path; @@ -164,7 +169,8 @@ typedef struct { const char* taesd_path; const char* control_net_path; const char* lora_model_dir; - const char* embedding_dir; + const sd_embedding_t* embeddings; + uint32_t embedding_count; const char* photo_maker_path; const char* tensor_type_rules; bool vae_decode_only; From 583a02e29e9101905ef2bc550256363cb4cbaea9 Mon Sep 17 00:00:00 2001 From: stduhpf Date: Tue, 9 Dec 2025 15:00:45 +0100 Subject: [PATCH 02/49] feat: add Flux.2 VAE proj matrix for previews (#1017) --- latent-preview.h | 79 +++++++++++++++++++++++++++++++++++++++----- stable-diffusion.cpp | 18 +++++++--- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/latent-preview.h b/latent-preview.h index 97409a7d8..2c54c3b5e 100644 --- a/latent-preview.h +++ b/latent-preview.h @@ -91,6 +91,41 @@ const float flux_latent_rgb_proj[16][3] = { {-0.111849f, -0.055589f, -0.032361f}}; float flux_latent_rgb_bias[3] = {0.024600f, -0.006937f, -0.008089f}; +const float flux2_latent_rgb_proj[32][3] = { + {0.000736f, -0.008385f, -0.019710f}, + {-0.001352f, -0.016392f, 0.020693f}, + {-0.006376f, 0.002428f, 0.036736f}, + {0.039384f, 0.074167f, 0.119789f}, + {0.007464f, -0.005705f, -0.004734f}, + {-0.004086f, 0.005287f, -0.000409f}, + {-0.032835f, 0.050802f, -0.028120f}, + {-0.003158f, -0.000835f, 0.000406f}, + {-0.112840f, -0.084337f, -0.023083f}, + {0.001462f, -0.006656f, 0.000549f}, + {-0.009980f, -0.007480f, 0.009702f}, + {0.032540f, 0.000214f, -0.061388f}, + {0.011023f, 0.000694f, 0.007143f}, + {-0.001468f, -0.006723f, -0.001678f}, + {-0.005921f, -0.010320f, -0.003907f}, + {-0.028434f, 0.027584f, 0.018457f}, + {0.014349f, 0.011523f, 0.000441f}, + {0.009874f, 0.003081f, 0.001507f}, + {0.002218f, 0.005712f, 0.001563f}, + {0.053010f, -0.019844f, 0.008683f}, + {-0.002507f, 0.005384f, 0.000938f}, + {-0.002177f, -0.011366f, 0.003559f}, + {-0.000261f, 0.015121f, -0.003240f}, + {-0.003944f, -0.002083f, 0.005043f}, + {-0.009138f, 0.011336f, 0.003781f}, + {0.011429f, 0.003985f, -0.003855f}, + {0.010518f, -0.005586f, 0.010131f}, + {0.007883f, 0.002912f, -0.001473f}, + {-0.003318f, -0.003160f, 0.003684f}, + {-0.034560f, -0.008740f, 0.012996f}, + {0.000166f, 0.001079f, -0.012153f}, + {0.017772f, 0.000937f, -0.011953f}}; +float flux2_latent_rgb_bias[3] = {-0.028738f, -0.098463f, -0.107619f}; + // This one was taken straight from // https://github.com/Stability-AI/sd3.5/blob/8565799a3b41eb0c7ba976d18375f0f753f56402/sd3_impls.py#L288-L303 // (MiT Licence) @@ -128,16 +163,42 @@ const float sd_latent_rgb_proj[4][3] = { {-0.178022f, -0.200862f, -0.678514f}}; float sd_latent_rgb_bias[3] = {-0.017478f, -0.055834f, -0.105825f}; -void preview_latent_video(uint8_t* buffer, struct ggml_tensor* latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int width, int height, int frames, int dim) { +void preview_latent_video(uint8_t* buffer, struct ggml_tensor* latents, const float (*latent_rgb_proj)[3], const float latent_rgb_bias[3], int patch_size) { size_t buffer_head = 0; + + uint32_t latent_width = latents->ne[0]; + uint32_t latent_height = latents->ne[1]; + uint32_t dim = latents->ne[ggml_n_dims(latents) - 1]; + uint32_t frames = 1; + if (ggml_n_dims(latents) == 4) { + frames = latents->ne[2]; + } + + uint32_t rgb_width = latent_width * patch_size; + uint32_t rgb_height = latent_height * patch_size; + + uint32_t unpatched_dim = dim / (patch_size * patch_size); + for (int k = 0; k < frames; k++) { - for (int j = 0; j < height; j++) { - for (int i = 0; i < width; i++) { - size_t latent_id = (i * latents->nb[0] + j * latents->nb[1] + k * latents->nb[2]); + for (int rgb_x = 0; rgb_x < rgb_width; rgb_x++) { + for (int rgb_y = 0; rgb_y < rgb_height; rgb_y++) { + int latent_x = rgb_x / patch_size; + int latent_y = rgb_y / patch_size; + + int channel_offset = 0; + if (patch_size > 1) { + channel_offset = ((rgb_y % patch_size) * patch_size + (rgb_x % patch_size)); + } + + size_t latent_id = (latent_x * latents->nb[0] + latent_y * latents->nb[1] + k * latents->nb[2]); + + // should be incremented by 1 for each pixel + size_t pixel_id = k * rgb_width * rgb_height + rgb_y * rgb_width + rgb_x; + float r = 0, g = 0, b = 0; if (latent_rgb_proj != nullptr) { - for (int d = 0; d < dim; d++) { - float value = *(float*)((char*)latents->data + latent_id + d * latents->nb[ggml_n_dims(latents) - 1]); + for (int d = 0; d < unpatched_dim; d++) { + float value = *(float*)((char*)latents->data + latent_id + (d * patch_size * patch_size + channel_offset) * latents->nb[ggml_n_dims(latents) - 1]); r += value * latent_rgb_proj[d][0]; g += value * latent_rgb_proj[d][1]; b += value * latent_rgb_proj[d][2]; @@ -164,9 +225,9 @@ void preview_latent_video(uint8_t* buffer, struct ggml_tensor* latents, const fl g = g >= 0 ? g <= 1 ? g : 1 : 0; b = b >= 0 ? b <= 1 ? b : 1 : 0; - buffer[buffer_head++] = (uint8_t)(r * 255); - buffer[buffer_head++] = (uint8_t)(g * 255); - buffer[buffer_head++] = (uint8_t)(b * 255); + buffer[pixel_id * 3 + 0] = (uint8_t)(r * 255); + buffer[pixel_id * 3 + 1] = (uint8_t)(g * 255); + buffer[pixel_id * 3 + 2] = (uint8_t)(b * 255); } } } diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 9b054244f..6ee0fcaf7 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -1326,10 +1326,17 @@ class StableDiffusionGGML { uint32_t dim = latents->ne[ggml_n_dims(latents) - 1]; if (preview_mode == PREVIEW_PROJ) { + int64_t patch_sz = 1; const float(*latent_rgb_proj)[channel] = nullptr; float* latent_rgb_bias = nullptr; - if (dim == 48) { + if (dim == 128) { + if (sd_version_is_flux2(version)) { + latent_rgb_proj = flux2_latent_rgb_proj; + latent_rgb_bias = flux2_latent_rgb_bias; + patch_sz = 2; + } + } else if (dim == 48) { if (sd_version_is_wan(version)) { latent_rgb_proj = wan_22_latent_rgb_proj; latent_rgb_bias = wan_22_latent_rgb_bias; @@ -1382,12 +1389,15 @@ class StableDiffusionGGML { frames = latents->ne[2]; } - uint8_t* data = (uint8_t*)malloc(frames * width * height * channel * sizeof(uint8_t)); + uint32_t img_width = width * patch_sz; + uint32_t img_height = height * patch_sz; + + uint8_t* data = (uint8_t*)malloc(frames * img_width * img_height * channel * sizeof(uint8_t)); - preview_latent_video(data, latents, latent_rgb_proj, latent_rgb_bias, width, height, frames, dim); + preview_latent_video(data, latents, latent_rgb_proj, latent_rgb_bias, patch_sz); sd_image_t* images = (sd_image_t*)malloc(frames * sizeof(sd_image_t)); for (int i = 0; i < frames; i++) { - images[i] = {width, height, channel, data + i * width * height * channel}; + images[i] = {img_width, img_height, channel, data + i * img_width * img_height * channel}; } step_callback(step, frames, images, is_noisy, step_callback_data); free(data); From a90843672937ba6f4b9dfeb4a71585cc882c8938 Mon Sep 17 00:00:00 2001 From: wuhei <30749950+wuhei@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:06:16 +0100 Subject: [PATCH 03/49] docs: update download link for Stable Diffusion v1.5 (#1063) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77740f460..01b750bfd 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ API and command-line option may change frequently.*** ### Download model weights - download weights(.ckpt or .safetensors or .gguf). For example - - Stable Diffusion v1.5 from https://huggingface.co/runwayml/stable-diffusion-v1-5 + - Stable Diffusion v1.5 from https://huggingface.co/stable-diffusion-v1-5/stable-diffusion-v1-5 ```sh curl -L -O https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors From e72aea796e4bef9a59fb1238ae1acea46114a376 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Tue, 9 Dec 2025 11:38:54 -0300 Subject: [PATCH 04/49] feat: embed version string and git commit hash (#1008) --- CMakeLists.txt | 32 ++++++++++++++++++++++++++++++++ examples/cli/main.cpp | 18 ++++++++++++++++++ stable-diffusion.h | 3 +++ version.cpp | 20 ++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 version.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7dc36f0fd..8ea1c47b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,38 @@ file(GLOB SD_LIB_SOURCES "*.hpp" ) +find_program(GIT_EXE NAMES git git.exe NO_CMAKE_FIND_ROOT_PATH) +if(GIT_EXE) + execute_process(COMMAND ${GIT_EXE} describe --tags --abbrev=7 --dirty=+ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE SDCPP_BUILD_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + execute_process(COMMAND ${GIT_EXE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE SDCPP_BUILD_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) +endif() + +if(NOT SDCPP_BUILD_VERSION) + set(SDCPP_BUILD_VERSION unknown) +endif() +message(STATUS "stable-diffusion.cpp version ${SDCPP_BUILD_VERSION}") + +if(NOT SDCPP_BUILD_COMMIT) + set(SDCPP_BUILD_COMMIT unknown) +endif() +message(STATUS "stable-diffusion.cpp commit ${SDCPP_BUILD_COMMIT}") + +set_property( + SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/version.cpp + APPEND PROPERTY COMPILE_DEFINITIONS + SDCPP_BUILD_COMMIT=${SDCPP_BUILD_COMMIT} SDCPP_BUILD_VERSION=${SDCPP_BUILD_VERSION} +) + if(SD_BUILD_SHARED_LIBS) message("-- Build shared library") message(${SD_LIB_SOURCES}) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index acb198161..2829f4dfd 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -324,6 +324,7 @@ struct SDCliParams { std::string output_path = "output.png"; bool verbose = false; + bool version = false; bool canny_preprocess = false; preview_t preview_method = PREVIEW_NONE; @@ -366,6 +367,10 @@ struct SDCliParams { "--verbose", "print extra info", true, &verbose}, + {"", + "--version", + "print stable-diffusion.cpp version", + true, &version}, {"", "--color", "colors the logging tags according to level", @@ -1598,7 +1603,12 @@ struct SDGenerationParams { } }; +static std::string version_string() { + return std::string("stable-diffusion.cpp version ") + sd_version() + ", commit " + sd_commit(); +} + void print_usage(int argc, const char* argv[], const std::vector& options_list) { + std::cout << version_string() << "\n"; std::cout << "Usage: " << argv[0] << " [options]\n\n"; std::cout << "CLI Options:\n"; options_list[0].print(); @@ -1881,11 +1891,19 @@ void step_callback(int step, int frame_count, sd_image_t* image, bool is_noisy, } int main(int argc, const char* argv[]) { + if (argc > 1 && std::string(argv[1]) == "--version") { + std::cout << version_string() << "\n"; + return EXIT_SUCCESS; + } + SDCliParams cli_params; SDContextParams ctx_params; SDGenerationParams gen_params; parse_args(argc, argv, cli_params, ctx_params, gen_params); + if (cli_params.verbose || cli_params.version) { + std::cout << version_string() << "\n"; + } if (gen_params.video_frames > 4) { size_t last_dot_pos = cli_params.preview_path.find_last_of("."); std::string base_path = cli_params.preview_path; diff --git a/stable-diffusion.h b/stable-diffusion.h index 7c72c70a7..cc5f4fa76 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -359,6 +359,9 @@ SD_API bool preprocess_canny(sd_image_t image, float strong, bool inverse); +SD_API const char* sd_commit(void); +SD_API const char* sd_version(void); + #ifdef __cplusplus } #endif diff --git a/version.cpp b/version.cpp new file mode 100644 index 000000000..97dc8426b --- /dev/null +++ b/version.cpp @@ -0,0 +1,20 @@ +#include "stable-diffusion.h" + +#ifndef SDCPP_BUILD_COMMIT +#define SDCPP_BUILD_COMMIT unknown +#endif + +#ifndef SDCPP_BUILD_VERSION +#define SDCPP_BUILD_VERSION unknown +#endif + +#define STRINGIZE2(x) #x +#define STRINGIZE(x) STRINGIZE2(x) + +const char* sd_commit(void) { + return STRINGIZE(SDCPP_BUILD_COMMIT); +} + +const char* sd_version(void) { + return STRINGIZE(SDCPP_BUILD_VERSION); +} From d939f6e86a47701a3b89574dc5306094aa689e83 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 10 Dec 2025 00:26:07 +0800 Subject: [PATCH 05/49] refactor: optimize the handling of LoRA models (#1070) --- clip.hpp | 25 -------- examples/cli/main.cpp | 146 +++++++++++++++++++++++++++++++++++++++--- stable-diffusion.cpp | 51 ++++++--------- stable-diffusion.h | 10 +++ util.cpp | 34 ---------- util.h | 1 - 6 files changed, 169 insertions(+), 98 deletions(-) diff --git a/clip.hpp b/clip.hpp index 0070833cb..24c94f1bb 100644 --- a/clip.hpp +++ b/clip.hpp @@ -7,31 +7,6 @@ /*================================================== CLIPTokenizer ===================================================*/ -__STATIC_INLINE__ std::pair, std::string> extract_and_remove_lora(std::string text) { - std::regex re("]+)>"); - std::smatch matches; - std::unordered_map filename2multiplier; - - while (std::regex_search(text, matches, re)) { - std::string filename = matches[1].str(); - float multiplier = std::stof(matches[2].str()); - - text = std::regex_replace(text, re, "", std::regex_constants::format_first_only); - - if (multiplier == 0.f) { - continue; - } - - if (filename2multiplier.find(filename) == filename2multiplier.end()) { - filename2multiplier[filename] = multiplier; - } else { - filename2multiplier[filename] += multiplier; - } - } - - return std::make_pair(filename2multiplier, text); -} - __STATIC_INLINE__ std::vector> bytes_to_unicode() { std::vector> byte_unicode_pairs; std::set byte_set; diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 2829f4dfd..c55da3536 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -507,7 +507,7 @@ struct SDContextParams { std::string lora_model_dir; std::map embedding_map; - std::vector embedding_array; + std::vector embedding_vec; rng_type_t rng_type = CUDA_RNG; rng_type_t sampler_rng_type = RNG_TYPE_COUNT; @@ -952,13 +952,13 @@ struct SDContextParams { } sd_ctx_params_t to_sd_ctx_params_t(bool vae_decode_only, bool free_params_immediately, bool taesd_preview) { - embedding_array.clear(); - embedding_array.reserve(embedding_map.size()); + embedding_vec.clear(); + embedding_vec.reserve(embedding_map.size()); for (const auto& kv : embedding_map) { sd_embedding_t item; item.name = kv.first.c_str(); item.path = kv.second.c_str(); - embedding_array.emplace_back(item); + embedding_vec.emplace_back(item); } sd_ctx_params_t sd_ctx_params = { @@ -975,8 +975,8 @@ struct SDContextParams { taesd_path.c_str(), control_net_path.c_str(), lora_model_dir.c_str(), - embedding_array.data(), - static_cast(embedding_array.size()), + embedding_vec.data(), + static_cast(embedding_vec.size()), photo_maker_path.c_str(), tensor_type_rules.c_str(), vae_decode_only, @@ -1030,6 +1030,15 @@ static std::string vec_str_to_string(const std::vector& v) { return oss.str(); } +static bool is_absolute_path(const std::string& p) { +#ifdef _WIN32 + // Windows: C:/path or C:\path + return p.size() > 1 && std::isalpha(static_cast(p[0])) && p[1] == ':'; +#else + return !p.empty() && p[0] == '/'; +#endif +} + struct SDGenerationParams { std::string prompt; std::string negative_prompt; @@ -1072,6 +1081,10 @@ struct SDGenerationParams { int upscale_repeats = 1; + std::map lora_map; + std::map high_noise_lora_map; + std::vector lora_vec; + SDGenerationParams() { sd_sample_params_init(&sample_params); sd_sample_params_init(&high_noise_sample_params); @@ -1442,7 +1455,88 @@ struct SDGenerationParams { return options; } - bool process_and_check(SDMode mode) { + void extract_and_remove_lora(const std::string& lora_model_dir) { + static const std::regex re(R"(]+):([^>]+)>)"); + static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; + std::smatch m; + + std::string tmp = prompt; + + while (std::regex_search(tmp, m, re)) { + std::string raw_path = m[1].str(); + const std::string raw_mul = m[2].str(); + + float mul = 0.f; + try { + mul = std::stof(raw_mul); + } catch (...) { + tmp = m.suffix().str(); + prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); + continue; + } + + bool is_high_noise = false; + static const std::string prefix = "|high_noise|"; + if (raw_path.rfind(prefix, 0) == 0) { + raw_path.erase(0, prefix.size()); + is_high_noise = true; + } + + fs::path final_path; + if (is_absolute_path(raw_path)) { + final_path = raw_path; + } else { + final_path = fs::path(lora_model_dir) / raw_path; + } + if (!fs::exists(final_path)) { + bool found = false; + for (const auto& ext : valid_ext) { + fs::path try_path = final_path; + try_path += ext; + if (fs::exists(try_path)) { + final_path = try_path; + found = true; + break; + } + } + if (!found) { + printf("can not found lora %s\n", final_path.lexically_normal().string().c_str()); + tmp = m.suffix().str(); + prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); + continue; + } + } + + const std::string key = final_path.lexically_normal().string(); + + if (is_high_noise) + high_noise_lora_map[key] += mul; + else + lora_map[key] += mul; + + prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); + + tmp = m.suffix().str(); + } + + for (const auto& kv : lora_map) { + sd_lora_t item; + item.is_high_noise = false; + item.path = kv.first.c_str(); + item.multiplier = kv.second; + lora_vec.emplace_back(item); + } + + for (const auto& kv : high_noise_lora_map) { + sd_lora_t item; + item.is_high_noise = true; + item.path = kv.first.c_str(); + item.multiplier = kv.second; + lora_vec.emplace_back(item); + } + } + + bool process_and_check(SDMode mode, const std::string& lora_model_dir) { if (width <= 0) { fprintf(stderr, "error: the width must be greater than 0\n"); return false; @@ -1553,14 +1647,44 @@ struct SDGenerationParams { seed = rand(); } + extract_and_remove_lora(lora_model_dir); + return true; } std::string to_string() const { char* sample_params_str = sd_sample_params_to_str(&sample_params); char* high_noise_sample_params_str = sd_sample_params_to_str(&high_noise_sample_params); + + std::ostringstream lora_ss; + lora_ss << "{\n"; + for (auto it = lora_map.begin(); it != lora_map.end(); ++it) { + lora_ss << " \"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != lora_map.end()) { + lora_ss << ","; + } + lora_ss << "\n"; + } + lora_ss << " }"; + std::string loras_str = lora_ss.str(); + + lora_ss = std::ostringstream(); + ; + lora_ss << "{\n"; + for (auto it = high_noise_lora_map.begin(); it != high_noise_lora_map.end(); ++it) { + lora_ss << " \"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != high_noise_lora_map.end()) { + lora_ss << ","; + } + lora_ss << "\n"; + } + lora_ss << " }"; + std::string high_noise_loras_str = lora_ss.str(); + std::ostringstream oss; oss << "SDGenerationParams {\n" + << " loras: \"" << loras_str << "\",\n" + << " high_noise_loras: \"" << high_noise_loras_str << "\",\n" << " prompt: \"" << prompt << "\",\n" << " negative_prompt: \"" << negative_prompt << "\",\n" << " clip_skip: " << clip_skip << ",\n" @@ -1626,7 +1750,9 @@ void parse_args(int argc, const char** argv, SDCliParams& cli_params, SDContextP exit(cli_params.normal_exit ? 0 : 1); } - if (!cli_params.process_and_check() || !ctx_params.process_and_check(cli_params.mode) || !gen_params.process_and_check(cli_params.mode)) { + if (!cli_params.process_and_check() || + !ctx_params.process_and_check(cli_params.mode) || + !gen_params.process_and_check(cli_params.mode, ctx_params.lora_model_dir)) { print_usage(argc, argv, options_vec); exit(1); } @@ -2139,6 +2265,8 @@ int main(int argc, const char* argv[]) { if (cli_params.mode == IMG_GEN) { sd_img_gen_params_t img_gen_params = { + gen_params.lora_vec.data(), + static_cast(gen_params.lora_vec.size()), gen_params.prompt.c_str(), gen_params.negative_prompt.c_str(), gen_params.clip_skip, @@ -2170,6 +2298,8 @@ int main(int argc, const char* argv[]) { num_results = gen_params.batch_count; } else if (cli_params.mode == VID_GEN) { sd_vid_gen_params_t vid_gen_params = { + gen_params.lora_vec.data(), + static_cast(gen_params.lora_vec.size()), gen_params.prompt.c_str(), gen_params.negative_prompt.c_str(), gen_params.clip_skip, diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 6ee0fcaf7..d381bf6e3 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -937,28 +937,17 @@ class StableDiffusionGGML { float multiplier, ggml_backend_t backend, LoraModel::filter_t lora_tensor_filter = nullptr) { - std::string lora_name = lora_id; - std::string high_noise_tag = "|high_noise|"; - bool is_high_noise = false; - if (starts_with(lora_name, high_noise_tag)) { - lora_name = lora_name.substr(high_noise_tag.size()); + std::string lora_path = lora_id; + static std::string high_noise_tag = "|high_noise|"; + bool is_high_noise = false; + if (starts_with(lora_path, high_noise_tag)) { + lora_path = lora_path.substr(high_noise_tag.size()); is_high_noise = true; - LOG_DEBUG("high noise lora: %s", lora_name.c_str()); - } - std::string st_file_path = path_join(lora_model_dir, lora_name + ".safetensors"); - std::string ckpt_file_path = path_join(lora_model_dir, lora_name + ".ckpt"); - std::string file_path; - if (file_exists(st_file_path)) { - file_path = st_file_path; - } else if (file_exists(ckpt_file_path)) { - file_path = ckpt_file_path; - } else { - LOG_WARN("can not find %s or %s for lora %s", st_file_path.c_str(), ckpt_file_path.c_str(), lora_name.c_str()); - return nullptr; + LOG_DEBUG("high noise lora: %s", lora_path.c_str()); } - auto lora = std::make_shared(lora_id, backend, file_path, is_high_noise ? "model.high_noise_" : "", version); + auto lora = std::make_shared(lora_id, backend, lora_path, is_high_noise ? "model.high_noise_" : "", version); if (!lora->load_from_file(n_threads, lora_tensor_filter)) { - LOG_WARN("load lora tensors from %s failed", file_path.c_str()); + LOG_WARN("load lora tensors from %s failed", lora_path.c_str()); return nullptr; } @@ -1143,12 +1132,15 @@ class StableDiffusionGGML { } } - std::string apply_loras_from_prompt(const std::string& prompt) { - auto result_pair = extract_and_remove_lora(prompt); - std::unordered_map lora_f2m = result_pair.first; // lora_name -> multiplier - - for (auto& kv : lora_f2m) { - LOG_DEBUG("lora %s:%.2f", kv.first.c_str(), kv.second); + void apply_loras(const sd_lora_t* loras, uint32_t lora_count) { + std::unordered_map lora_f2m; + for (int i = 0; i < lora_count; i++) { + std::string lora_id = SAFE_STR(loras[i].path); + if (loras[i].is_high_noise) { + lora_id = "|high_noise|" + lora_id; + } + lora_f2m[lora_id] = loras[i].multiplier; + LOG_DEBUG("lora %s:%.2f", lora_id.c_str(), loras[i].multiplier); } int64_t t0 = ggml_time_ms(); if (apply_lora_immediately) { @@ -1159,9 +1151,7 @@ class StableDiffusionGGML { int64_t t1 = ggml_time_ms(); if (!lora_f2m.empty()) { LOG_INFO("apply_loras completed, taking %.2fs", (t1 - t0) * 1.0f / 1000); - LOG_DEBUG("prompt after extract and remove lora: \"%s\"", result_pair.second.c_str()); } - return result_pair.second; } ggml_tensor* id_encoder(ggml_context* work_ctx, @@ -2815,8 +2805,6 @@ sd_image_t* generate_image_internal(sd_ctx_t* sd_ctx, int sample_steps = sigmas.size() - 1; int64_t t0 = ggml_time_ms(); - // Apply lora - prompt = sd_ctx->sd->apply_loras_from_prompt(prompt); // Photo Maker std::string prompt_text_only; @@ -3188,6 +3176,9 @@ sd_image_t* generate_image(sd_ctx_t* sd_ctx, const sd_img_gen_params_t* sd_img_g size_t t0 = ggml_time_ms(); + // Apply lora + sd_ctx->sd->apply_loras(sd_img_gen_params->loras, sd_img_gen_params->lora_count); + enum sample_method_t sample_method = sd_img_gen_params->sample_params.sample_method; if (sample_method == SAMPLE_METHOD_COUNT) { sample_method = sd_get_default_sample_method(sd_ctx); @@ -3487,7 +3478,7 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s int64_t t0 = ggml_time_ms(); // Apply lora - prompt = sd_ctx->sd->apply_loras_from_prompt(prompt); + sd_ctx->sd->apply_loras(sd_vid_gen_params->loras, sd_vid_gen_params->lora_count); ggml_tensor* init_latent = nullptr; ggml_tensor* clip_vision_output = nullptr; diff --git a/stable-diffusion.h b/stable-diffusion.h index cc5f4fa76..601b79f93 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -242,6 +242,14 @@ typedef struct { } sd_easycache_params_t; typedef struct { + bool is_high_noise; + float multiplier; + const char* path; +} sd_lora_t; + +typedef struct { + const sd_lora_t* loras; + uint32_t lora_count; const char* prompt; const char* negative_prompt; int clip_skip; @@ -265,6 +273,8 @@ typedef struct { } sd_img_gen_params_t; typedef struct { + const sd_lora_t* loras; + uint32_t lora_count; const char* prompt; const char* negative_prompt; int clip_skip; diff --git a/util.cpp b/util.cpp index 4a59852e2..680ff8046 100644 --- a/util.cpp +++ b/util.cpp @@ -95,20 +95,6 @@ bool is_directory(const std::string& path) { return (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY)); } -std::string get_full_path(const std::string& dir, const std::string& filename) { - std::string full_path = dir + "\\" + filename; - - WIN32_FIND_DATA find_file_data; - HANDLE hFind = FindFirstFile(full_path.c_str(), &find_file_data); - - if (hFind != INVALID_HANDLE_VALUE) { - FindClose(hFind); - return full_path; - } else { - return ""; - } -} - #else // Unix #include #include @@ -123,26 +109,6 @@ bool is_directory(const std::string& path) { return (stat(path.c_str(), &buffer) == 0 && S_ISDIR(buffer.st_mode)); } -// TODO: add windows version -std::string get_full_path(const std::string& dir, const std::string& filename) { - DIR* dp = opendir(dir.c_str()); - - if (dp != nullptr) { - struct dirent* entry; - - while ((entry = readdir(dp)) != nullptr) { - if (strcasecmp(entry->d_name, filename.c_str()) == 0) { - closedir(dp); - return dir + "/" + entry->d_name; - } - } - - closedir(dp); - } - - return ""; -} - #endif // get_num_physical_cores is copy from diff --git a/util.h b/util.h index 61ca9334a..dd4a0c30f 100644 --- a/util.h +++ b/util.h @@ -22,7 +22,6 @@ int round_up_to(int value, int base); bool file_exists(const std::string& filename); bool is_directory(const std::string& path); -std::string get_full_path(const std::string& dir, const std::string& filename); std::u32string utf8_to_utf32(const std::string& utf8_str); std::string utf32_to_utf8(const std::u32string& utf32_str); From 1ac5a616de3f472d4bccea085d783922e4daa770 Mon Sep 17 00:00:00 2001 From: Pedrito Date: Wed, 10 Dec 2025 15:25:19 +0100 Subject: [PATCH 06/49] feat: support custom upscale tile size (#896) --- esrgan.hpp | 3 ++- examples/cli/main.cpp | 15 +++++++++++++-- stable-diffusion.h | 3 ++- upscaler.cpp | 16 ++++++++++------ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/esrgan.hpp b/esrgan.hpp index 4cac95686..961e84f89 100644 --- a/esrgan.hpp +++ b/esrgan.hpp @@ -156,9 +156,10 @@ struct ESRGAN : public GGMLRunner { ESRGAN(ggml_backend_t backend, bool offload_params_to_cpu, + int tile_size = 128, const String2TensorStorage& tensor_storage_map = {}) : GGMLRunner(backend, offload_params_to_cpu) { - // rrdb_net will be created in load_from_file + this->tile_size = tile_size; } std::string get_desc() override { diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index c55da3536..49b202fda 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -1079,7 +1079,8 @@ struct SDGenerationParams { std::string pm_id_embed_path; float pm_style_strength = 20.f; - int upscale_repeats = 1; + int upscale_repeats = 1; + int upscale_tile_size = 128; std::map lora_map; std::map high_noise_lora_map; @@ -1176,6 +1177,10 @@ struct SDGenerationParams { "--upscale-repeats", "Run the ESRGAN upscaler this many times (default: 1)", &upscale_repeats}, + {"", + "--upscale-tile-size", + "tile size for ESRGAN upscaling (default: 128)", + &upscale_tile_size}, }; options.float_options = { @@ -1635,6 +1640,10 @@ struct SDGenerationParams { return false; } + if (upscale_tile_size < 1) { + return false; + } + if (mode == UPSCALE) { if (init_image_path.length() == 0) { fprintf(stderr, "error: upscale mode needs an init image (--init-img)\n"); @@ -1720,6 +1729,7 @@ struct SDGenerationParams { << " control_strength: " << control_strength << ",\n" << " seed: " << seed << ",\n" << " upscale_repeats: " << upscale_repeats << ",\n" + << " upscale_tile_size: " << upscale_tile_size << ",\n" << "}"; free(sample_params_str); free(high_noise_sample_params_str); @@ -2336,7 +2346,8 @@ int main(int argc, const char* argv[]) { upscaler_ctx_t* upscaler_ctx = new_upscaler_ctx(ctx_params.esrgan_path.c_str(), ctx_params.offload_params_to_cpu, ctx_params.diffusion_conv_direct, - ctx_params.n_threads); + ctx_params.n_threads, + gen_params.upscale_tile_size); if (upscaler_ctx == nullptr) { printf("new_upscaler_ctx failed\n"); diff --git a/stable-diffusion.h b/stable-diffusion.h index 601b79f93..2da70bd77 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -347,7 +347,8 @@ typedef struct upscaler_ctx_t upscaler_ctx_t; SD_API upscaler_ctx_t* new_upscaler_ctx(const char* esrgan_path, bool offload_params_to_cpu, bool direct, - int n_threads); + int n_threads, + int tile_size); SD_API void free_upscaler_ctx(upscaler_ctx_t* upscaler_ctx); SD_API sd_image_t upscale(upscaler_ctx_t* upscaler_ctx, diff --git a/upscaler.cpp b/upscaler.cpp index 62c0d29ad..29ac981e6 100644 --- a/upscaler.cpp +++ b/upscaler.cpp @@ -9,12 +9,15 @@ struct UpscalerGGML { std::shared_ptr esrgan_upscaler; std::string esrgan_path; int n_threads; - bool direct = false; + bool direct = false; + int tile_size = 128; UpscalerGGML(int n_threads, - bool direct = false) + bool direct = false, + int tile_size = 128) : n_threads(n_threads), - direct(direct) { + direct(direct), + tile_size(tile_size) { } bool load_from_file(const std::string& esrgan_path, @@ -51,7 +54,7 @@ struct UpscalerGGML { backend = ggml_backend_cpu_init(); } LOG_INFO("Upscaler weight type: %s", ggml_type_name(model_data_type)); - esrgan_upscaler = std::make_shared(backend, offload_params_to_cpu, model_loader.get_tensor_storage_map()); + esrgan_upscaler = std::make_shared(backend, offload_params_to_cpu, tile_size, model_loader.get_tensor_storage_map()); if (direct) { esrgan_upscaler->set_conv2d_direct_enabled(true); } @@ -113,14 +116,15 @@ struct upscaler_ctx_t { upscaler_ctx_t* new_upscaler_ctx(const char* esrgan_path_c_str, bool offload_params_to_cpu, bool direct, - int n_threads) { + int n_threads, + int tile_size) { upscaler_ctx_t* upscaler_ctx = (upscaler_ctx_t*)malloc(sizeof(upscaler_ctx_t)); if (upscaler_ctx == nullptr) { return nullptr; } std::string esrgan_path(esrgan_path_c_str); - upscaler_ctx->upscaler = new UpscalerGGML(n_threads, direct); + upscaler_ctx->upscaler = new UpscalerGGML(n_threads, direct, tile_size); if (upscaler_ctx->upscaler == nullptr) { return nullptr; } From 8823dc48bcc1598eb9671da7b69e45338d0cc5a5 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 10 Dec 2025 23:15:08 +0800 Subject: [PATCH 07/49] feat: align the spatial size to the corresponding multiple (#1073) --- ggml_extend.hpp | 8 +++++++ stable-diffusion.cpp | 53 ++++++++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/ggml_extend.hpp b/ggml_extend.hpp index 2b4ce5d85..5024eb911 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -60,6 +60,14 @@ #define SD_UNUSED(x) (void)(x) #endif +__STATIC_INLINE__ int align_up_offset(int n, int multiple) { + return (multiple - n % multiple) % multiple; +} + +__STATIC_INLINE__ int align_up(int n, int multiple) { + return n + align_up_offset(n, multiple); +} + __STATIC_INLINE__ void ggml_log_callback_default(ggml_log_level level, const char* text, void*) { switch (level) { case GGML_LOG_LEVEL_DEBUG: diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index d381bf6e3..1ef851247 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -1898,6 +1898,18 @@ class StableDiffusionGGML { return vae_scale_factor; } + int get_diffusion_model_down_factor() { + int down_factor = 8; // unet + if (sd_version_is_dit(version)) { + if (sd_version_is_wan(version)) { + down_factor = 2; + } else { + down_factor = 1; + } + } + return down_factor; + } + int get_latent_channel() { int latent_channel = 4; if (sd_version_is_dit(version)) { @@ -3133,22 +3145,19 @@ sd_image_t* generate_image(sd_ctx_t* sd_ctx, const sd_img_gen_params_t* sd_img_g sd_ctx->sd->vae_tiling_params = sd_img_gen_params->vae_tiling_params; int width = sd_img_gen_params->width; int height = sd_img_gen_params->height; - int vae_scale_factor = sd_ctx->sd->get_vae_scale_factor(); - if (sd_version_is_dit(sd_ctx->sd->version)) { - if (width % 16 || height % 16) { - LOG_ERROR("Image dimensions must be must be a multiple of 16 on each axis for %s models. (Got %dx%d)", - model_version_to_str[sd_ctx->sd->version], - width, - height); - return nullptr; - } - } else if (width % 64 || height % 64) { - LOG_ERROR("Image dimensions must be must be a multiple of 64 on each axis for %s models. (Got %dx%d)", - model_version_to_str[sd_ctx->sd->version], - width, - height); - return nullptr; + + int vae_scale_factor = sd_ctx->sd->get_vae_scale_factor(); + int diffusion_model_down_factor = sd_ctx->sd->get_diffusion_model_down_factor(); + int spatial_multiple = vae_scale_factor * diffusion_model_down_factor; + + int width_offset = align_up_offset(width, spatial_multiple); + int height_offset = align_up_offset(height, spatial_multiple); + if (width_offset > 0 || height_offset > 0) { + width += width_offset; + height += height_offset; + LOG_WARN("align up %dx%d to %dx%d (multiple=%d)", sd_img_gen_params->width, sd_img_gen_params->height, width, height, spatial_multiple); } + LOG_DEBUG("generate_image %dx%d", width, height); if (sd_ctx == nullptr || sd_img_gen_params == nullptr) { return nullptr; @@ -3422,9 +3431,19 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s int frames = sd_vid_gen_params->video_frames; frames = (frames - 1) / 4 * 4 + 1; int sample_steps = sd_vid_gen_params->sample_params.sample_steps; - LOG_INFO("generate_video %dx%dx%d", width, height, frames); - int vae_scale_factor = sd_ctx->sd->get_vae_scale_factor(); + int vae_scale_factor = sd_ctx->sd->get_vae_scale_factor(); + int diffusion_model_down_factor = sd_ctx->sd->get_diffusion_model_down_factor(); + int spatial_multiple = vae_scale_factor * diffusion_model_down_factor; + + int width_offset = align_up_offset(width, spatial_multiple); + int height_offset = align_up_offset(height, spatial_multiple); + if (width_offset > 0 || height_offset > 0) { + width += width_offset; + height += height_offset; + LOG_WARN("align up %dx%d to %dx%d (multiple=%d)", sd_vid_gen_params->width, sd_vid_gen_params->height, width, height, spatial_multiple); + } + LOG_INFO("generate_video %dx%dx%d", width, height, frames); enum sample_method_t sample_method = sd_vid_gen_params->sample_params.sample_method; if (sample_method == SAMPLE_METHOD_COUNT) { From a3a88fc9b2e3bc5df63e4ca16040952e809d8354 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Fri, 12 Dec 2025 11:36:54 -0300 Subject: [PATCH 08/49] fix: avoid crash loading LoRAs with bf16 weights (#1077) --- ggml_extend.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ggml_extend.hpp b/ggml_extend.hpp index 5024eb911..07b9bfbf0 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -1400,10 +1400,14 @@ __STATIC_INLINE__ void ggml_ext_backend_tensor_get_and_sync(ggml_backend_t backe } __STATIC_INLINE__ float ggml_ext_backend_tensor_get_f32(ggml_tensor* tensor) { - GGML_ASSERT(tensor->type == GGML_TYPE_F32 || tensor->type == GGML_TYPE_F16 || tensor->type == GGML_TYPE_I32); + GGML_ASSERT(tensor->type == GGML_TYPE_F32 || tensor->type == GGML_TYPE_F16 || tensor->type == GGML_TYPE_I32 || tensor->type == GGML_TYPE_BF16); float value; if (tensor->type == GGML_TYPE_F32) { ggml_backend_tensor_get(tensor, &value, 0, sizeof(value)); + } else if (tensor->type == GGML_TYPE_BF16) { + ggml_bf16_t bf16_value; + ggml_backend_tensor_get(tensor, &bf16_value, 0, sizeof(bf16_value)); + value = ggml_bf16_to_fp32(bf16_value); } else if (tensor->type == GGML_TYPE_F16) { ggml_fp16_t f16_value; ggml_backend_tensor_get(tensor, &f16_value, 0, sizeof(f16_value)); From 11ab095230b2b67210f5da4d901588d56c71fe3a Mon Sep 17 00:00:00 2001 From: leejet Date: Fri, 12 Dec 2025 23:08:12 +0800 Subject: [PATCH 09/49] fix: resolve embedding loading issue when calling generate_image multiple times (#1078) --- conditioner.hpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/conditioner.hpp b/conditioner.hpp index 55e1502e8..45db314b9 100644 --- a/conditioner.hpp +++ b/conditioner.hpp @@ -60,7 +60,7 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { int32_t num_custom_embeddings = 0; int32_t num_custom_embeddings_2 = 0; std::vector token_embed_custom; - std::vector readed_embeddings; + std::map> embedding_pos_map; FrozenCLIPEmbedderWithCustomWords(ggml_backend_t backend, bool offload_params_to_cpu, @@ -123,14 +123,17 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { } bool load_embedding(std::string embd_name, std::string embd_path, std::vector& bpe_tokens) { - // the order matters ModelLoader model_loader; if (!model_loader.init_from_file_and_convert_name(embd_path)) { LOG_ERROR("embedding '%s' failed", embd_name.c_str()); return false; } - if (std::find(readed_embeddings.begin(), readed_embeddings.end(), embd_name) != readed_embeddings.end()) { + auto iter = embedding_pos_map.find(embd_name); + if (iter != embedding_pos_map.end()) { LOG_DEBUG("embedding already read in: %s", embd_name.c_str()); + for (int i = iter->second.first; i < iter->second.second; i++) { + bpe_tokens.push_back(text_model->model.vocab_size + i); + } return true; } struct ggml_init_params params; @@ -161,7 +164,7 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { return true; }; model_loader.load_tensors(on_load, 1); - readed_embeddings.push_back(embd_name); + int pos_start = num_custom_embeddings; if (embd) { int64_t hidden_size = text_model->model.hidden_size; token_embed_custom.resize(token_embed_custom.size() + ggml_nbytes(embd)); @@ -188,6 +191,11 @@ struct FrozenCLIPEmbedderWithCustomWords : public Conditioner { } LOG_DEBUG("embedding '%s' applied, custom embeddings: %i (text model 2)", embd_name.c_str(), num_custom_embeddings_2); } + int pos_end = num_custom_embeddings; + if (pos_end == pos_start) { + return false; + } + embedding_pos_map[embd_name] = std::pair{pos_start, pos_end}; return true; } From 2aecdd57ca017abfbae951de862f19905e4558e0 Mon Sep 17 00:00:00 2001 From: leejet Date: Sat, 13 Dec 2025 13:53:21 +0800 Subject: [PATCH 10/49] feat: simple openai image generation api compatiple server (#1037) --- Dockerfile | 4 +- Dockerfile.musa | 4 +- Dockerfile.sycl | 4 +- README.md | 2 +- docs/chroma.md | 4 +- docs/chroma_radiance.md | 2 +- docs/docker.md | 4 +- docs/esrgan.md | 2 +- docs/flux.md | 8 +- docs/flux2.md | 2 +- docs/hipBLAS_on_Windows.md | 2 +- docs/kontext.md | 4 +- docs/lcm.md | 2 +- docs/lora.md | 2 +- docs/ovis_image.md | 2 +- docs/photo_maker.md | 2 +- docs/quantization_and_gguf.md | 2 +- docs/qwen_image.md | 2 +- docs/qwen_image_edit.md | 4 +- docs/sd.md | 14 +- docs/sd3.md | 2 +- docs/taesd.md | 2 +- docs/wan.md | 34 +- docs/z_image.md | 2 +- examples/CMakeLists.txt | 3 +- examples/cli/CMakeLists.txt | 2 +- examples/cli/README.md | 2 +- examples/cli/main.cpp | 1651 +--- examples/common/common.hpp | 1752 ++++ examples/server/CMakeLists.txt | 6 + examples/server/README.md | 63 + examples/server/main.cpp | 715 ++ format-code.sh | 2 +- thirdparty/README.md | 9 +- thirdparty/httplib.h | 13303 +++++++++++++++++++++++++++++++ 35 files changed, 15915 insertions(+), 1705 deletions(-) create mode 100644 examples/common/common.hpp create mode 100644 examples/server/CMakeLists.txt create mode 100644 examples/server/README.md create mode 100644 examples/server/main.cpp create mode 100644 thirdparty/httplib.h diff --git a/Dockerfile b/Dockerfile index 417335793..da73021c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,6 @@ RUN apt-get update && \ apt-get install --yes --no-install-recommends libgomp1 && \ apt-get clean -COPY --from=build /sd.cpp/build/bin/sd /sd +COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli -ENTRYPOINT [ "/sd" ] \ No newline at end of file +ENTRYPOINT [ "/sd-cli" ] \ No newline at end of file diff --git a/Dockerfile.musa b/Dockerfile.musa index 9722ac70d..0eac3d7f2 100644 --- a/Dockerfile.musa +++ b/Dockerfile.musa @@ -18,6 +18,6 @@ RUN mkdir build && cd build && \ FROM mthreads/musa:${MUSA_VERSION}-runtime-ubuntu${UBUNTU_VERSION}-amd64 as runtime -COPY --from=build /sd.cpp/build/bin/sd /sd +COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli -ENTRYPOINT [ "/sd" ] \ No newline at end of file +ENTRYPOINT [ "/sd-cli" ] \ No newline at end of file diff --git a/Dockerfile.sycl b/Dockerfile.sycl index 1b855d6e4..6bcb91dad 100644 --- a/Dockerfile.sycl +++ b/Dockerfile.sycl @@ -14,6 +14,6 @@ RUN mkdir build && cd build && \ FROM intel/oneapi-basekit:${SYCL_VERSION}-devel-ubuntu24.04 AS runtime -COPY --from=build /sd.cpp/build/bin/sd /sd +COPY --from=build /sd.cpp/build/bin/sd-cli /sd-cli -ENTRYPOINT [ "/sd" ] +ENTRYPOINT [ "/sd-cli" ] diff --git a/README.md b/README.md index 01b750bfd..aa29f8494 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ API and command-line option may change frequently.*** ### Generate an image with just one command ```sh -./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" +./bin/sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" ``` ***For detailed command-line arguments, check out [cli doc](./examples/cli/README.md).*** diff --git a/docs/chroma.md b/docs/chroma.md index 5aac64441..138a9cba7 100644 --- a/docs/chroma.md +++ b/docs/chroma.md @@ -15,7 +15,7 @@ You can run Chroma using stable-diffusion.cpp with a GPU that has 6GB or even 4G You can download the preconverted gguf weights from [silveroxides/Chroma-GGUF](https://huggingface.co/silveroxides/Chroma-GGUF), this way you don't have to do the conversion yourself. ``` -.\bin\Release\sd.exe -M convert -m ..\..\ComfyUI\models\unet\chroma-unlocked-v40.safetensors -o ..\models\chroma-unlocked-v40-q8_0.gguf -v --type q8_0 +.\bin\Release\sd-cli.exe -M convert -m ..\..\ComfyUI\models\unet\chroma-unlocked-v40.safetensors -o ..\models\chroma-unlocked-v40-q8_0.gguf -v --type q8_0 ``` ## Run @@ -24,7 +24,7 @@ You can download the preconverted gguf weights from [silveroxides/Chroma-GGUF](h For example: ``` - .\bin\Release\sd.exe --diffusion-model ..\models\chroma-unlocked-v40-q8_0.gguf --vae ..\models\ae.sft --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma.cpp'" --cfg-scale 4.0 --sampling-method euler -v --chroma-disable-dit-mask --clip-on-cpu + .\bin\Release\sd-cli.exe --diffusion-model ..\models\chroma-unlocked-v40-q8_0.gguf --vae ..\models\ae.sft --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma.cpp'" --cfg-scale 4.0 --sampling-method euler -v --chroma-disable-dit-mask --clip-on-cpu ``` ![](../assets/flux/chroma_v40.png) diff --git a/docs/chroma_radiance.md b/docs/chroma_radiance.md index a343520bf..fe3f1c3c1 100644 --- a/docs/chroma_radiance.md +++ b/docs/chroma_radiance.md @@ -12,7 +12,7 @@ ## Examples ``` -.\bin\Release\sd.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Chroma1-Radiance-v0.4-Q8_0.gguf --t5xxl ..\..\ComfyUI\models\clip\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma radiance cpp'" --cfg-scale 4.0 --sampling-method euler -v +.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Chroma1-Radiance-v0.4-Q8_0.gguf --t5xxl ..\..\ComfyUI\models\clip\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma radiance cpp'" --cfg-scale 4.0 --sampling-method euler -v ``` Chroma1-Radiance diff --git a/docs/docker.md b/docs/docker.md index 96e9838de..26a5f7145 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -9,7 +9,7 @@ docker build -t sd . ### Run ```shell -docker run -v /path/to/models:/models -v /path/to/output/:/output sd [args...] +docker run -v /path/to/models:/models -v /path/to/output/:/output sd-cli [args...] # For example -# docker run -v ./models:/models -v ./build:/output sd -m /models/sd-v1-4.ckpt -p "a lovely cat" -v -o /output/output.png +# docker run -v ./models:/models -v ./build:/output sd-cli -m /models/sd-v1-4.ckpt -p "a lovely cat" -v -o /output/output.png ``` \ No newline at end of file diff --git a/docs/esrgan.md b/docs/esrgan.md index 21f2af4ae..77231726c 100644 --- a/docs/esrgan.md +++ b/docs/esrgan.md @@ -5,5 +5,5 @@ You can use ESRGAN to upscale the generated images. At the moment, only the [Rea - Specify the model path using the `--upscale-model PATH` parameter. example: ```bash -sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --upscale-model ../models/RealESRGAN_x4plus_anime_6B.pth +sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --upscale-model ../models/RealESRGAN_x4plus_anime_6B.pth ``` diff --git a/docs/flux.md b/docs/flux.md index a070d231b..23564d4fc 100644 --- a/docs/flux.md +++ b/docs/flux.md @@ -17,7 +17,7 @@ You can download the preconverted gguf weights from [FLUX.1-dev-gguf](https://hu For example: ``` -.\bin\Release\sd.exe -M convert -m ..\..\ComfyUI\models\unet\flux1-dev.sft -o ..\models\flux1-dev-q8_0.gguf -v --type q8_0 +.\bin\Release\sd-cli.exe -M convert -m ..\..\ComfyUI\models\unet\flux1-dev.sft -o ..\models\flux1-dev-q8_0.gguf -v --type q8_0 ``` ## Run @@ -28,7 +28,7 @@ For example: For example: ``` - .\bin\Release\sd.exe --diffusion-model ..\models\flux1-dev-q8_0.gguf --vae ..\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu + .\bin\Release\sd-cli.exe --diffusion-model ..\models\flux1-dev-q8_0.gguf --vae ..\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu ``` Using formats of different precisions will yield results of varying quality. @@ -44,7 +44,7 @@ Using formats of different precisions will yield results of varying quality. ``` - .\bin\Release\sd.exe --diffusion-model ..\models\flux1-schnell-q8_0.gguf --vae ..\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --steps 4 --clip-on-cpu + .\bin\Release\sd-cli.exe --diffusion-model ..\models\flux1-schnell-q8_0.gguf --vae ..\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --steps 4 --clip-on-cpu ``` | q8_0 | @@ -60,7 +60,7 @@ Since many flux LoRA training libraries have used various LoRA naming formats, i - LoRA model from https://huggingface.co/XLabs-AI/flux-lora-collection/tree/main (using comfy converted version!!!) ``` -.\bin\Release\sd.exe --diffusion-model ..\models\flux1-dev-q8_0.gguf --vae ...\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --lora-model-dir ../models --clip-on-cpu +.\bin\Release\sd-cli.exe --diffusion-model ..\models\flux1-dev-q8_0.gguf --vae ...\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --lora-model-dir ../models --clip-on-cpu ``` ![output](../assets/flux/flux1-dev-q8_0%20with%20lora.png) diff --git a/docs/flux2.md b/docs/flux2.md index 3b93a57e2..0c2c6d2b7 100644 --- a/docs/flux2.md +++ b/docs/flux2.md @@ -12,7 +12,7 @@ ## Examples ``` -.\bin\Release\sd.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux2-dev-Q4_K_S.gguf --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Mistral-Small-3.2-24B-Instruct-2506-Q4_K_M.gguf -r .\kontext_input.png -p "change 'flux.cpp' to 'flux2-dev.cpp'" --cfg-scale 1.0 --sampling-method euler -v --diffusion-fa --offload-to-cpu +.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\flux2-dev-Q4_K_S.gguf --vae ..\..\ComfyUI\models\vae\flux2_ae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Mistral-Small-3.2-24B-Instruct-2506-Q4_K_M.gguf -r .\kontext_input.png -p "change 'flux.cpp' to 'flux2-dev.cpp'" --cfg-scale 1.0 --sampling-method euler -v --diffusion-fa --offload-to-cpu ``` flux2 example diff --git a/docs/hipBLAS_on_Windows.md b/docs/hipBLAS_on_Windows.md index cff0aacc7..b5105ad19 100644 --- a/docs/hipBLAS_on_Windows.md +++ b/docs/hipBLAS_on_Windows.md @@ -82,4 +82,4 @@ cmake .. -G "Ninja" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DSD_H cmake --build . --config Release ``` -If everything went OK, `build\bin\sd.exe` file should appear. +If everything went OK, `build\bin\sd-cli.exe` file should appear. diff --git a/docs/kontext.md b/docs/kontext.md index 58898066b..a0e9f3c81 100644 --- a/docs/kontext.md +++ b/docs/kontext.md @@ -16,7 +16,7 @@ You can run Kontext using stable-diffusion.cpp with a GPU that has 6GB or even 4 You can download the preconverted gguf weights from [FLUX.1-Kontext-dev-GGUF](https://huggingface.co/QuantStack/FLUX.1-Kontext-dev-GGUF), this way you don't have to do the conversion yourself. ``` -.\bin\Release\sd.exe -M convert -m ..\..\ComfyUI\models\unet\flux1-kontext-dev.safetensors -o ..\models\flux1-kontext-dev-q8_0.gguf -v --type q8_0 +.\bin\Release\sd-cli.exe -M convert -m ..\..\ComfyUI\models\unet\flux1-kontext-dev.safetensors -o ..\models\flux1-kontext-dev-q8_0.gguf -v --type q8_0 ``` ## Run @@ -27,7 +27,7 @@ You can download the preconverted gguf weights from [FLUX.1-Kontext-dev-GGUF](ht For example: ``` - .\bin\Release\sd.exe -r .\flux1-dev-q8_0.png --diffusion-model ..\models\flux1-kontext-dev-q8_0.gguf --vae ..\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "change 'flux.cpp' to 'kontext.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu + .\bin\Release\sd-cli.exe -r .\flux1-dev-q8_0.png --diffusion-model ..\models\flux1-kontext-dev-q8_0.gguf --vae ..\models\ae.sft --clip_l ..\models\clip_l.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -p "change 'flux.cpp' to 'kontext.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu ``` diff --git a/docs/lcm.md b/docs/lcm.md index 14a363406..3a11938aa 100644 --- a/docs/lcm.md +++ b/docs/lcm.md @@ -7,7 +7,7 @@ Here's a simple example: ``` -./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --steps 4 --lora-model-dir ../models -v --cfg-scale 1 +./bin/sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --steps 4 --lora-model-dir ../models -v --cfg-scale 1 ``` | without LCM-LoRA (--cfg-scale 7) | with LCM-LoRA (--cfg-scale 1) | diff --git a/docs/lora.md b/docs/lora.md index fe4fbc0b3..d6dc71899 100644 --- a/docs/lora.md +++ b/docs/lora.md @@ -7,7 +7,7 @@ Here's a simple example: ``` -./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --lora-model-dir ../models +./bin/sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --lora-model-dir ../models ``` `../models/marblesh.safetensors` or `../models/marblesh.ckpt` will be applied to the model diff --git a/docs/ovis_image.md b/docs/ovis_image.md index 20e641a82..5bd3e8ea3 100644 --- a/docs/ovis_image.md +++ b/docs/ovis_image.md @@ -13,7 +13,7 @@ ## Examples ``` -.\bin\Release\sd.exe --diffusion-model ovis_image-Q4_0.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\ovis_2.5.safetensors -p "a lovely cat" --cfg-scale 5.0 -v --offload-to-cpu --diffusion-fa +.\bin\Release\sd-cli.exe --diffusion-model ovis_image-Q4_0.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\ovis_2.5.safetensors -p "a lovely cat" --cfg-scale 5.0 -v --offload-to-cpu --diffusion-fa ``` ovis image example \ No newline at end of file diff --git a/docs/photo_maker.md b/docs/photo_maker.md index 31203ef73..1a3463ef4 100644 --- a/docs/photo_maker.md +++ b/docs/photo_maker.md @@ -27,7 +27,7 @@ If on low memory GPUs (<= 8GB), recommend running with ```--vae-on-cpu``` option Example: ```bash -bin/sd -m ../models/sdxlUnstableDiffusers_v11.safetensors --vae ../models/sdxl_vae.safetensors --photo-maker ../models/photomaker-v1.safetensors --pm-id-images-dir ../assets/photomaker_examples/scarletthead_woman -p "a girl img, retro futurism, retro game art style but extremely beautiful, intricate details, masterpiece, best quality, space-themed, cosmic, celestial, stars, galaxies, nebulas, planets, science fiction, highly detailed" -n "realistic, photo-realistic, worst quality, greyscale, bad anatomy, bad hands, error, text" --cfg-scale 5.0 --sampling-method euler -H 1024 -W 1024 --pm-style-strength 10 --vae-on-cpu --steps 50 +bin/sd-cli -m ../models/sdxlUnstableDiffusers_v11.safetensors --vae ../models/sdxl_vae.safetensors --photo-maker ../models/photomaker-v1.safetensors --pm-id-images-dir ../assets/photomaker_examples/scarletthead_woman -p "a girl img, retro futurism, retro game art style but extremely beautiful, intricate details, masterpiece, best quality, space-themed, cosmic, celestial, stars, galaxies, nebulas, planets, science fiction, highly detailed" -n "realistic, photo-realistic, worst quality, greyscale, bad anatomy, bad hands, error, text" --cfg-scale 5.0 --sampling-method euler -H 1024 -W 1024 --pm-style-strength 10 --vae-on-cpu --steps 50 ``` ## PhotoMaker Version 2 diff --git a/docs/quantization_and_gguf.md b/docs/quantization_and_gguf.md index 4f131555d..fd7f7ee06 100644 --- a/docs/quantization_and_gguf.md +++ b/docs/quantization_and_gguf.md @@ -23,5 +23,5 @@ You can also convert weights in the formats `ckpt/safetensors/diffusers` to gguf For example: ```sh -./bin/sd -M convert -m ../models/v1-5-pruned-emaonly.safetensors -o ../models/v1-5-pruned-emaonly.q8_0.gguf -v --type q8_0 +./bin/sd-cli -M convert -m ../models/v1-5-pruned-emaonly.safetensors -o ../models/v1-5-pruned-emaonly.q8_0.gguf -v --type q8_0 ``` \ No newline at end of file diff --git a/docs/qwen_image.md b/docs/qwen_image.md index cfd9da2a7..f12421f47 100644 --- a/docs/qwen_image.md +++ b/docs/qwen_image.md @@ -14,7 +14,7 @@ ## Examples ``` -.\bin\Release\sd.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\qwen-image-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct-Q8_0.gguf -p '一个穿着"QWEN"标志的T恤的中国美女正拿着黑色的马克笔面相镜头微笑。她身后的玻璃板上手写体写着 “一、Qwen-Image的技术路线: 探索视觉生成基础模型的极限,开创理解与生成一体化的未来。二、Qwen-Image的模型特色:1、复杂文字渲染。支持中英渲染、自动布局; 2、精准图像编辑。支持文字编辑、物体增减、风格变换。三、Qwen-Image的未来愿景:赋能专业内容创作、助力生成式AI发展。”' --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu -H 1024 -W 1024 --diffusion-fa --flow-shift 3 +.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\qwen-image-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct-Q8_0.gguf -p '一个穿着"QWEN"标志的T恤的中国美女正拿着黑色的马克笔面相镜头微笑。她身后的玻璃板上手写体写着 “一、Qwen-Image的技术路线: 探索视觉生成基础模型的极限,开创理解与生成一体化的未来。二、Qwen-Image的模型特色:1、复杂文字渲染。支持中英渲染、自动布局; 2、精准图像编辑。支持文字编辑、物体增减、风格变换。三、Qwen-Image的未来愿景:赋能专业内容创作、助力生成式AI发展。”' --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu -H 1024 -W 1024 --diffusion-fa --flow-shift 3 ``` qwen example diff --git a/docs/qwen_image_edit.md b/docs/qwen_image_edit.md index 36be1c926..d376a2830 100644 --- a/docs/qwen_image_edit.md +++ b/docs/qwen_image_edit.md @@ -20,7 +20,7 @@ ### Qwen Image Edit ``` -.\bin\Release\sd.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Qwen_Image_Edit-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_2.5_vl_7b.safetensors --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'edit.cpp'" --seed 1118877715456453 +.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Qwen_Image_Edit-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_2.5_vl_7b.safetensors --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'edit.cpp'" --seed 1118877715456453 ``` qwen_image_edit @@ -29,7 +29,7 @@ ### Qwen Image Edit 2509 ``` -.\bin\Release\sd.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Qwen-Image-Edit-2509-Q4_K_S.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct-Q8_0.gguf --llm_vision ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'Qwen Image Edit 2509'" +.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Qwen-Image-Edit-2509-Q4_K_S.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct-Q8_0.gguf --llm_vision ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'Qwen Image Edit 2509'" ``` qwen_image_edit_2509 \ No newline at end of file diff --git a/docs/sd.md b/docs/sd.md index f95c47287..dde76f3b8 100644 --- a/docs/sd.md +++ b/docs/sd.md @@ -9,12 +9,12 @@ ### txt2img example ```sh -./bin/sd -m ../models/sd-v1-4.ckpt -p "a lovely cat" -# ./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" -# ./bin/sd -m ../models/sd_xl_base_1.0.safetensors --vae ../models/sdxl_vae-fp16-fix.safetensors -H 1024 -W 1024 -p "a lovely cat" -v -# ./bin/sd -m ../models/sd3_medium_incl_clips_t5xxlfp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says \"Stable Diffusion CPP\"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu -# ./bin/sd --diffusion-model ../models/flux1-dev-q3_k.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu -# ./bin/sd -m ..\models\sd3.5_large.safetensors --clip_l ..\models\clip_l.safetensors --clip_g ..\models\clip_g.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says \"Stable diffusion 3.5 Large\"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu +./bin/sd-cli -m ../models/sd-v1-4.ckpt -p "a lovely cat" +# ./bin/sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" +# ./bin/sd-cli -m ../models/sd_xl_base_1.0.safetensors --vae ../models/sdxl_vae-fp16-fix.safetensors -H 1024 -W 1024 -p "a lovely cat" -v +# ./bin/sd-cli -m ../models/sd3_medium_incl_clips_t5xxlfp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says \"Stable Diffusion CPP\"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu +# ./bin/sd-cli --diffusion-model ../models/flux1-dev-q3_k.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu +# ./bin/sd-cli -m ..\models\sd3.5_large.safetensors --clip_l ..\models\clip_l.safetensors --clip_g ..\models\clip_g.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says \"Stable diffusion 3.5 Large\"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu ``` Using formats of different precisions will yield results of varying quality. @@ -29,7 +29,7 @@ Using formats of different precisions will yield results of varying quality. ``` -./bin/sd -m ../models/sd-v1-4.ckpt -p "cat with blue eyes" -i ./output.png -o ./img2img_output.png --strength 0.4 +./bin/sd-cli -m ../models/sd-v1-4.ckpt -p "cat with blue eyes" -i ./output.png -o ./img2img_output.png --strength 0.4 ```

diff --git a/docs/sd3.md b/docs/sd3.md index 2c1f8ff33..3893659f9 100644 --- a/docs/sd3.md +++ b/docs/sd3.md @@ -14,7 +14,7 @@ For example: ``` -.\bin\Release\sd.exe -m ..\models\sd3.5_large.safetensors --clip_l ..\models\clip_l.safetensors --clip_g ..\models\clip_g.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says \"Stable diffusion 3.5 Large\"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu +.\bin\Release\sd-cli.exe -m ..\models\sd3.5_large.safetensors --clip_l ..\models\clip_l.safetensors --clip_g ..\models\clip_g.safetensors --t5xxl ..\models\t5xxl_fp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says \"Stable diffusion 3.5 Large\"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu ``` ![](../assets/sd3.5_large.png) \ No newline at end of file diff --git a/docs/taesd.md b/docs/taesd.md index 5f8d4d9f0..5160b7934 100644 --- a/docs/taesd.md +++ b/docs/taesd.md @@ -13,5 +13,5 @@ curl -L -O https://huggingface.co/madebyollin/taesd/resolve/main/diffusion_pytor - Specify the model path using the `--taesd PATH` parameter. example: ```bash -sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --taesd ../models/diffusion_pytorch_model.safetensors +sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --taesd ../models/diffusion_pytorch_model.safetensors ``` \ No newline at end of file diff --git a/docs/wan.md b/docs/wan.md index 5bde71c7e..ce15ba589 100644 --- a/docs/wan.md +++ b/docs/wan.md @@ -52,7 +52,7 @@ ### Wan2.1 T2V 1.3B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1_t2v_1.3B_fp16.safetensors --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1_t2v_1.3B_fp16.safetensors --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0 ``` @@ -60,7 +60,7 @@ ### Wan2.1 T2V 14B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-t2v-14b-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --video-frames 33 --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-t2v-14b-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --video-frames 33 --flow-shift 3.0 ``` @@ -70,7 +70,7 @@ ### Wan2.1 I2V 14B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-i2v-14b-480p-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf --clip_vision ..\..\ComfyUI\models\clip_vision\clip_vision_h.safetensors -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --video-frames 33 --offload-to-cpu -i ..\assets\cat_with_sd_cpp_42.png --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-i2v-14b-480p-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf --clip_vision ..\..\ComfyUI\models\clip_vision\clip_vision_h.safetensors -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --video-frames 33 --offload-to-cpu -i ..\assets\cat_with_sd_cpp_42.png --flow-shift 3.0 ``` @@ -78,7 +78,7 @@ ### Wan2.2 T2V A14B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --video-frames 33 --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --video-frames 33 --flow-shift 3.0 ``` @@ -86,7 +86,7 @@ ### Wan2.2 I2V A14B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --video-frames 33 --offload-to-cpu -i ..\assets\cat_with_sd_cpp_42.png --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --video-frames 33 --offload-to-cpu -i ..\assets\cat_with_sd_cpp_42.png --flow-shift 3.0 ``` @@ -94,7 +94,7 @@ ### Wan2.2 T2V A14B T2I ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --flow-shift 3.0 ``` Wan2 2_14B_t2i @@ -102,7 +102,7 @@ ### Wan2.2 T2V 14B with Lora ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 4 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 4 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --lora-model-dir ..\..\ComfyUI\models\loras --video-frames 33 --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 3.5 --sampling-method euler --steps 4 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 4 -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --lora-model-dir ..\..\ComfyUI\models\loras --video-frames 33 --flow-shift 3.0 ``` @@ -114,7 +114,7 @@ #### T2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.2_ti2v_5B_fp16.safetensors --vae ..\..\ComfyUI\models\vae\wan2.2_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --offload-to-cpu --video-frames 33 --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.2_ti2v_5B_fp16.safetensors --vae ..\..\ComfyUI\models\vae\wan2.2_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --offload-to-cpu --video-frames 33 --flow-shift 3.0 ``` @@ -122,7 +122,7 @@ #### I2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.2_ti2v_5B_fp16.safetensors --vae ..\..\ComfyUI\models\vae\wan2.2_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --offload-to-cpu --video-frames 33 -i ..\assets\cat_with_sd_cpp_42.png --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.2_ti2v_5B_fp16.safetensors --vae ..\..\ComfyUI\models\vae\wan2.2_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --offload-to-cpu --video-frames 33 -i ..\assets\cat_with_sd_cpp_42.png --flow-shift 3.0 ``` @@ -130,7 +130,7 @@ ### Wan2.1 FLF2V 14B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-flf2v-14b-720p-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf --clip_vision ..\..\ComfyUI\models\clip_vision\clip_vision_h.safetensors -p "glass flower blossom" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --video-frames 33 --offload-to-cpu --init-img ..\..\ComfyUI\input\start_image.png --end-img ..\..\ComfyUI\input\end_image.png --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-flf2v-14b-720p-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf --clip_vision ..\..\ComfyUI\models\clip_vision\clip_vision_h.safetensors -p "glass flower blossom" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --video-frames 33 --offload-to-cpu --init-img ..\..\ComfyUI\input\start_image.png --end-img ..\..\ComfyUI\input\end_image.png --flow-shift 3.0 ``` @@ -139,7 +139,7 @@ ### Wan2.2 FLF2V 14B ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -p "glass flower blossom" -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --video-frames 33 --offload-to-cpu --init-img ..\..\ComfyUI\input\start_image.png --end-img ..\..\ComfyUI\input\end_image.png --flow-shift 3.0 +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf --high-noise-diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf --cfg-scale 3.5 --sampling-method euler --steps 10 --high-noise-cfg-scale 3.5 --high-noise-sampling-method euler --high-noise-steps 8 -v -p "glass flower blossom" -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa --video-frames 33 --offload-to-cpu --init-img ..\..\ComfyUI\input\start_image.png --end-img ..\..\ComfyUI\input\end_image.png --flow-shift 3.0 ``` @@ -149,7 +149,7 @@ #### T2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-vace-1.3b-q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --video-frames 1 --offload-to-cpu +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-vace-1.3b-q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --video-frames 1 --offload-to-cpu ``` @@ -158,7 +158,7 @@ #### R2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-vace-1.3b-q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa -i ..\assets\cat_with_sd_cpp_42.png --video-frames 33 --offload-to-cpu +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-vace-1.3b-q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa -i ..\assets\cat_with_sd_cpp_42.png --video-frames 33 --offload-to-cpu ``` @@ -169,7 +169,7 @@ ``` mkdir post+depth ffmpeg -i ..\..\ComfyUI\input\post+depth.mp4 -qscale:v 1 -vf fps=8 post+depth\frame_%04d.jpg -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-vace-1.3b-q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "The girl is dancing in a sea of flowers, slowly moving her hands. There is a close - up shot of her upper body. The character is surrounded by other transparent glass flowers in the style of Nicoletta Ceccoli, creating a beautiful, surreal, and emotionally expressive movie scene with a white. transparent feel and a dreamyl atmosphere." --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa -i ..\..\ComfyUI\input\dance_girl.jpg --control-video ./post+depth --video-frames 33 --offload-to-cpu +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\wan2.1-vace-1.3b-q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "The girl is dancing in a sea of flowers, slowly moving her hands. There is a close - up shot of her upper body. The character is surrounded by other transparent glass flowers in the style of Nicoletta Ceccoli, creating a beautiful, surreal, and emotionally expressive movie scene with a white. transparent feel and a dreamyl atmosphere." --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa -i ..\..\ComfyUI\input\dance_girl.jpg --control-video ./post+depth --video-frames 33 --offload-to-cpu ``` @@ -179,7 +179,7 @@ ffmpeg -i ..\..\ComfyUI\input\post+depth.mp4 -qscale:v 1 -vf fps=8 post+depth\fr #### T2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.1_14B_VACE-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --video-frames 33 --offload-to-cpu +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.1_14B_VACE-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --video-frames 33 --offload-to-cpu ``` @@ -188,7 +188,7 @@ ffmpeg -i ..\..\ComfyUI\input\post+depth.mp4 -qscale:v 1 -vf fps=8 post+depth\fr #### R2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.1_14B_VACE-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa -i ..\assets\cat_with_sd_cpp_42.png --video-frames 33 --offload-to-cpu +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.1_14B_VACE-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa -i ..\assets\cat_with_sd_cpp_42.png --video-frames 33 --offload-to-cpu ``` @@ -198,7 +198,7 @@ ffmpeg -i ..\..\ComfyUI\input\post+depth.mp4 -qscale:v 1 -vf fps=8 post+depth\fr #### V2V ``` -.\bin\Release\sd.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.1_14B_VACE-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "The girl is dancing in a sea of flowers, slowly moving her hands. There is a close - up shot of her upper body. The character is surrounded by other transparent glass flowers in the style of Nicoletta Ceccoli, creating a beautiful, surreal, and emotionally expressive movie scene with a white. transparent feel and a dreamyl atmosphere." --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa -i ..\..\ComfyUI\input\dance_girl.jpg --control-video ./post+depth --video-frames 33 --offload-to-cpu +.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\..\ComfyUI\models\diffusion_models\Wan2.1_14B_VACE-Q8_0.gguf --vae ..\..\ComfyUI\models\vae\wan_2.1_vae.safetensors --t5xxl ..\..\ComfyUI\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "The girl is dancing in a sea of flowers, slowly moving her hands. There is a close - up shot of her upper body. The character is surrounded by other transparent glass flowers in the style of Nicoletta Ceccoli, creating a beautiful, surreal, and emotionally expressive movie scene with a white. transparent feel and a dreamyl atmosphere." --cfg-scale 6.0 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部, 畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 480 -H 832 --diffusion-fa -i ..\..\ComfyUI\input\dance_girl.jpg --control-video ./post+depth --video-frames 33 --offload-to-cpu ``` diff --git a/docs/z_image.md b/docs/z_image.md index 73eacffa8..122f1f205 100644 --- a/docs/z_image.md +++ b/docs/z_image.md @@ -16,7 +16,7 @@ You can run Z-Image with stable-diffusion.cpp on GPUs with 4GB of VRAM — or ev ## Examples ``` -.\bin\Release\sd.exe --diffusion-model z_image_turbo-Q3_K.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\Qwen3-4B-Instruct-2507-Q4_K_M.gguf -p "A cinematic, melancholic photograph of a solitary hooded figure walking through a sprawling, rain-slicked metropolis at night. The city lights are a chaotic blur of neon orange and cool blue, reflecting on the wet asphalt. The scene evokes a sense of being a single component in a vast machine. Superimposed over the image in a sleek, modern, slightly glitched font is the philosophical quote: 'THE CITY IS A CIRCUIT BOARD, AND I AM A BROKEN TRANSISTOR.' -- moody, atmospheric, profound, dark academic" --cfg-scale 1.0 -v --offload-to-cpu --diffusion-fa -H 1024 -W 512 +.\bin\Release\sd-cli.exe --diffusion-model z_image_turbo-Q3_K.gguf --vae ..\..\ComfyUI\models\vae\ae.sft --llm ..\..\ComfyUI\models\text_encoders\Qwen3-4B-Instruct-2507-Q4_K_M.gguf -p "A cinematic, melancholic photograph of a solitary hooded figure walking through a sprawling, rain-slicked metropolis at night. The city lights are a chaotic blur of neon orange and cool blue, reflecting on the wet asphalt. The scene evokes a sense of being a single component in a vast machine. Superimposed over the image in a sleek, modern, slightly glitched font is the philosophical quote: 'THE CITY IS A CIRCUIT BOARD, AND I AM A BROKEN TRANSISTOR.' -- moody, atmospheric, profound, dark academic" --cfg-scale 1.0 -v --offload-to-cpu --diffusion-fa -H 1024 -W 512 ``` z-image example diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 81053f9e2..2dcd1d53a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,4 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}) -add_subdirectory(cli) \ No newline at end of file +add_subdirectory(cli) +add_subdirectory(server) \ No newline at end of file diff --git a/examples/cli/CMakeLists.txt b/examples/cli/CMakeLists.txt index 3f1532469..b30a2e888 100644 --- a/examples/cli/CMakeLists.txt +++ b/examples/cli/CMakeLists.txt @@ -1,4 +1,4 @@ -set(TARGET sd) +set(TARGET sd-cli) add_executable(${TARGET} main.cpp) install(TARGETS ${TARGET} RUNTIME) diff --git a/examples/cli/README.md b/examples/cli/README.md index add5e3eb7..f6a427851 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -1,7 +1,7 @@ # Run ``` -usage: ./bin/sd [options] +usage: ./bin/sd-cli [options] CLI Options: -o, --output path to write result image to (default: ./output.png) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 49b202fda..eaa2591e6 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -15,38 +15,10 @@ // #include "preprocessing.hpp" #include "stable-diffusion.h" -#define STB_IMAGE_IMPLEMENTATION -#define STB_IMAGE_STATIC -#include "stb_image.h" - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#define STB_IMAGE_WRITE_STATIC -#include "stb_image_write.h" - -#define STB_IMAGE_RESIZE_IMPLEMENTATION -#define STB_IMAGE_RESIZE_STATIC -#include "stb_image_resize.h" +#include "common/common.hpp" #include "avi_writer.h" -#if defined(_WIN32) -#define NOMINMAX -#include -#endif // _WIN32 - -#define SAFE_STR(s) ((s) ? (s) : "") -#define BOOL_STR(b) ((b) ? "true" : "false") - -namespace fs = std::filesystem; - -const char* modes_str[] = { - "img_gen", - "vid_gen", - "convert", - "upscale", -}; -#define SD_ALL_MODES_STR "img_gen, vid_gen, convert, upscale" - const char* previews_str[] = { "none", "proj", @@ -54,271 +26,6 @@ const char* previews_str[] = { "vae", }; -enum SDMode { - IMG_GEN, - VID_GEN, - CONVERT, - UPSCALE, - MODE_COUNT -}; - -#if defined(_WIN32) -static std::string utf16_to_utf8(const std::wstring& wstr) { - if (wstr.empty()) - return {}; - int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), - nullptr, 0, nullptr, nullptr); - if (size_needed <= 0) - throw std::runtime_error("UTF-16 to UTF-8 conversion failed"); - - std::string utf8(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), - (char*)utf8.data(), size_needed, nullptr, nullptr); - return utf8; -} - -static std::string argv_to_utf8(int index, const char** argv) { - int argc; - wchar_t** argv_w = CommandLineToArgvW(GetCommandLineW(), &argc); - if (!argv_w) - throw std::runtime_error("Failed to parse command line"); - - std::string result; - if (index < argc) { - result = utf16_to_utf8(argv_w[index]); - } - LocalFree(argv_w); - return result; -} - -#else // Linux / macOS -static std::string argv_to_utf8(int index, const char** argv) { - return std::string(argv[index]); -} - -#endif - -struct StringOption { - std::string short_name; - std::string long_name; - std::string desc; - std::string* target; -}; - -struct IntOption { - std::string short_name; - std::string long_name; - std::string desc; - int* target; -}; - -struct FloatOption { - std::string short_name; - std::string long_name; - std::string desc; - float* target; -}; - -struct BoolOption { - std::string short_name; - std::string long_name; - std::string desc; - bool keep_true; - bool* target; -}; - -struct ManualOption { - std::string short_name; - std::string long_name; - std::string desc; - std::function cb; -}; - -struct ArgOptions { - std::vector string_options; - std::vector int_options; - std::vector float_options; - std::vector bool_options; - std::vector manual_options; - - static std::string wrap_text(const std::string& text, size_t width, size_t indent) { - std::ostringstream oss; - size_t line_len = 0; - size_t pos = 0; - - while (pos < text.size()) { - // Preserve manual newlines - if (text[pos] == '\n') { - oss << '\n' - << std::string(indent, ' '); - line_len = indent; - ++pos; - continue; - } - - // Add the character - oss << text[pos]; - ++line_len; - ++pos; - - // If the current line exceeds width, try to break at the last space - if (line_len >= width) { - std::string current = oss.str(); - size_t back = current.size(); - - // Find the last space (for a clean break) - while (back > 0 && current[back - 1] != ' ' && current[back - 1] != '\n') - --back; - - // If found a space to break on - if (back > 0 && current[back - 1] != '\n') { - std::string before = current.substr(0, back - 1); - std::string after = current.substr(back); - oss.str(""); - oss.clear(); - oss << before << "\n" - << std::string(indent, ' ') << after; - } else { - // If no space found, just break at width - oss << "\n" - << std::string(indent, ' '); - } - line_len = indent; - } - } - - return oss.str(); - } - - void print() const { - constexpr size_t max_line_width = 120; - - struct Entry { - std::string names; - std::string desc; - }; - std::vector entries; - - auto add_entry = [&](const std::string& s, const std::string& l, - const std::string& desc, const std::string& hint = "") { - std::ostringstream ss; - if (!s.empty()) - ss << s; - if (!s.empty() && !l.empty()) - ss << ", "; - if (!l.empty()) - ss << l; - if (!hint.empty()) - ss << " " << hint; - entries.push_back({ss.str(), desc}); - }; - - for (auto& o : string_options) - add_entry(o.short_name, o.long_name, o.desc, ""); - for (auto& o : int_options) - add_entry(o.short_name, o.long_name, o.desc, ""); - for (auto& o : float_options) - add_entry(o.short_name, o.long_name, o.desc, ""); - for (auto& o : bool_options) - add_entry(o.short_name, o.long_name, o.desc, ""); - for (auto& o : manual_options) - add_entry(o.short_name, o.long_name, o.desc); - - size_t max_name_width = 0; - for (auto& e : entries) - max_name_width = std::max(max_name_width, e.names.size()); - - for (auto& e : entries) { - size_t indent = 2 + max_name_width + 4; - size_t desc_width = (max_line_width > indent ? max_line_width - indent : 40); - std::string wrapped_desc = wrap_text(e.desc, max_line_width, indent); - std::cout << " " << std::left << std::setw(static_cast(max_name_width) + 4) - << e.names << wrapped_desc << "\n"; - } - } -}; - -bool parse_options(int argc, const char** argv, const std::vector& options_list) { - bool invalid_arg = false; - std::string arg; - - auto match_and_apply = [&](auto& opts, auto&& apply_fn) -> bool { - for (auto& option : opts) { - if ((option.short_name.size() > 0 && arg == option.short_name) || - (option.long_name.size() > 0 && arg == option.long_name)) { - apply_fn(option); - return true; - } - } - return false; - }; - - for (int i = 1; i < argc; i++) { - arg = argv[i]; - bool found_arg = false; - - for (auto& options : options_list) { - if (match_and_apply(options.string_options, [&](auto& option) { - if (++i >= argc) { - invalid_arg = true; - return; - } - *option.target = argv_to_utf8(i, argv); - found_arg = true; - })) - break; - - if (match_and_apply(options.int_options, [&](auto& option) { - if (++i >= argc) { - invalid_arg = true; - return; - } - *option.target = std::stoi(argv[i]); - found_arg = true; - })) - break; - - if (match_and_apply(options.float_options, [&](auto& option) { - if (++i >= argc) { - invalid_arg = true; - return; - } - *option.target = std::stof(argv[i]); - found_arg = true; - })) - break; - - if (match_and_apply(options.bool_options, [&](auto& option) { - *option.target = option.keep_true ? true : false; - found_arg = true; - })) - break; - - if (match_and_apply(options.manual_options, [&](auto& option) { - int ret = option.cb(argc, argv, i); - if (ret < 0) { - invalid_arg = true; - return; - } - i += ret; - found_arg = true; - })) - break; - } - - if (invalid_arg) { - fprintf(stderr, "error: invalid parameter for argument: %s\n", arg.c_str()); - return false; - } - if (!found_arg) { - fprintf(stderr, "error: unknown argument: %s\n", arg.c_str()); - return false; - } - } - - return true; -} - struct SDCliParams { SDMode mode = IMG_GEN; std::string output_path = "output.png"; @@ -485,1262 +192,6 @@ struct SDCliParams { } }; -struct SDContextParams { - int n_threads = -1; - std::string model_path; - std::string clip_l_path; - std::string clip_g_path; - std::string clip_vision_path; - std::string t5xxl_path; - std::string llm_path; - std::string llm_vision_path; - std::string diffusion_model_path; - std::string high_noise_diffusion_model_path; - std::string vae_path; - std::string taesd_path; - std::string esrgan_path; - std::string control_net_path; - std::string embedding_dir; - std::string photo_maker_path; - sd_type_t wtype = SD_TYPE_COUNT; - std::string tensor_type_rules; - std::string lora_model_dir; - - std::map embedding_map; - std::vector embedding_vec; - - rng_type_t rng_type = CUDA_RNG; - rng_type_t sampler_rng_type = RNG_TYPE_COUNT; - bool offload_params_to_cpu = false; - bool control_net_cpu = false; - bool clip_on_cpu = false; - bool vae_on_cpu = false; - bool diffusion_flash_attn = false; - bool diffusion_conv_direct = false; - bool vae_conv_direct = false; - - bool chroma_use_dit_mask = true; - bool chroma_use_t5_mask = false; - int chroma_t5_mask_pad = 1; - - prediction_t prediction = PREDICTION_COUNT; - lora_apply_mode_t lora_apply_mode = LORA_APPLY_AUTO; - - sd_tiling_params_t vae_tiling_params = {false, 0, 0, 0.5f, 0.0f, 0.0f}; - bool force_sdxl_vae_conv_scale = false; - - float flow_shift = INFINITY; - - ArgOptions get_options() { - ArgOptions options; - options.string_options = { - {"-m", - "--model", - "path to full model", - &model_path}, - {"", - "--clip_l", - "path to the clip-l text encoder", &clip_l_path}, - {"", "--clip_g", - "path to the clip-g text encoder", - &clip_g_path}, - {"", - "--clip_vision", - "path to the clip-vision encoder", - &clip_vision_path}, - {"", - "--t5xxl", - "path to the t5xxl text encoder", - &t5xxl_path}, - {"", - "--llm", - "path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...)", - &llm_path}, - {"", - "--llm_vision", - "path to the llm vit", - &llm_vision_path}, - {"", - "--qwen2vl", - "alias of --llm. Deprecated.", - &llm_path}, - {"", - "--qwen2vl_vision", - "alias of --llm_vision. Deprecated.", - &llm_vision_path}, - {"", - "--diffusion-model", - "path to the standalone diffusion model", - &diffusion_model_path}, - {"", - "--high-noise-diffusion-model", - "path to the standalone high noise diffusion model", - &high_noise_diffusion_model_path}, - {"", - "--vae", - "path to standalone vae model", - &vae_path}, - {"", - "--taesd", - "path to taesd. Using Tiny AutoEncoder for fast decoding (low quality)", - &taesd_path}, - {"", - "--control-net", - "path to control net model", - &control_net_path}, - {"", - "--embd-dir", - "embeddings directory", - &embedding_dir}, - {"", - "--lora-model-dir", - "lora model directory", - &lora_model_dir}, - - {"", - "--tensor-type-rules", - "weight type per tensor pattern (example: \"^vae\\.=f16,model\\.=q8_0\")", - &tensor_type_rules}, - {"", - "--photo-maker", - "path to PHOTOMAKER model", - &photo_maker_path}, - {"", - "--upscale-model", - "path to esrgan model.", - &esrgan_path}, - }; - - options.int_options = { - {"-t", - "--threads", - "number of threads to use during computation (default: -1). " - "If threads <= 0, then threads will be set to the number of CPU physical cores", - &n_threads}, - {"", - "--chroma-t5-mask-pad", - "t5 mask pad size of chroma", - &chroma_t5_mask_pad}, - }; - - options.float_options = { - {"", - "--vae-tile-overlap", - "tile overlap for vae tiling, in fraction of tile size (default: 0.5)", - &vae_tiling_params.target_overlap}, - {"", - "--flow-shift", - "shift value for Flow models like SD3.x or WAN (default: auto)", - &flow_shift}, - }; - - options.bool_options = { - {"", - "--vae-tiling", - "process vae in tiles to reduce memory usage", - true, &vae_tiling_params.enabled}, - {"", - "--force-sdxl-vae-conv-scale", - "force use of conv scale on sdxl vae", - true, &force_sdxl_vae_conv_scale}, - {"", - "--offload-to-cpu", - "place the weights in RAM to save VRAM, and automatically load them into VRAM when needed", - true, &offload_params_to_cpu}, - {"", - "--control-net-cpu", - "keep controlnet in cpu (for low vram)", - true, &control_net_cpu}, - {"", - "--clip-on-cpu", - "keep clip in cpu (for low vram)", - true, &clip_on_cpu}, - {"", - "--vae-on-cpu", - "keep vae in cpu (for low vram)", - true, &vae_on_cpu}, - {"", - "--diffusion-fa", - "use flash attention in the diffusion model", - true, &diffusion_flash_attn}, - {"", - "--diffusion-conv-direct", - "use ggml_conv2d_direct in the diffusion model", - true, &diffusion_conv_direct}, - {"", - "--vae-conv-direct", - "use ggml_conv2d_direct in the vae model", - true, &vae_conv_direct}, - {"", - "--chroma-disable-dit-mask", - "disable dit mask for chroma", - false, &chroma_use_dit_mask}, - {"", - "--chroma-enable-t5-mask", - "enable t5 mask for chroma", - true, &chroma_use_t5_mask}, - }; - - auto on_type_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - wtype = str_to_sd_type(arg); - if (wtype == SD_TYPE_COUNT) { - fprintf(stderr, "error: invalid weight format %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_rng_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - rng_type = str_to_rng_type(arg); - if (rng_type == RNG_TYPE_COUNT) { - fprintf(stderr, "error: invalid rng type %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_sampler_rng_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - sampler_rng_type = str_to_rng_type(arg); - if (sampler_rng_type == RNG_TYPE_COUNT) { - fprintf(stderr, "error: invalid sampler rng type %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_prediction_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - prediction = str_to_prediction(arg); - if (prediction == PREDICTION_COUNT) { - fprintf(stderr, "error: invalid prediction type %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_lora_apply_mode_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - lora_apply_mode = str_to_lora_apply_mode(arg); - if (lora_apply_mode == LORA_APPLY_MODE_COUNT) { - fprintf(stderr, "error: invalid lora apply model %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_tile_size_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - std::string tile_size_str = argv[index]; - size_t x_pos = tile_size_str.find('x'); - try { - if (x_pos != std::string::npos) { - std::string tile_x_str = tile_size_str.substr(0, x_pos); - std::string tile_y_str = tile_size_str.substr(x_pos + 1); - vae_tiling_params.tile_size_x = std::stoi(tile_x_str); - vae_tiling_params.tile_size_y = std::stoi(tile_y_str); - } else { - vae_tiling_params.tile_size_x = vae_tiling_params.tile_size_y = std::stoi(tile_size_str); - } - } catch (const std::invalid_argument&) { - return -1; - } catch (const std::out_of_range&) { - return -1; - } - return 1; - }; - - auto on_relative_tile_size_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - std::string rel_size_str = argv[index]; - size_t x_pos = rel_size_str.find('x'); - try { - if (x_pos != std::string::npos) { - std::string rel_x_str = rel_size_str.substr(0, x_pos); - std::string rel_y_str = rel_size_str.substr(x_pos + 1); - vae_tiling_params.rel_size_x = std::stof(rel_x_str); - vae_tiling_params.rel_size_y = std::stof(rel_y_str); - } else { - vae_tiling_params.rel_size_x = vae_tiling_params.rel_size_y = std::stof(rel_size_str); - } - } catch (const std::invalid_argument&) { - return -1; - } catch (const std::out_of_range&) { - return -1; - } - return 1; - }; - - options.manual_options = { - {"", - "--type", - "weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). " - "If not specified, the default is the type of the weight file", - on_type_arg}, - {"", - "--rng", - "RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui)", - on_rng_arg}, - {"", - "--sampler-rng", - "sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng", - on_sampler_rng_arg}, - {"", - "--prediction", - "prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow]", - on_prediction_arg}, - {"", - "--lora-apply-mode", - "the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. " - "In auto mode, if the model weights contain any quantized parameters, the at_runtime mode will be used; otherwise, immediately will be used." - "The immediately mode may have precision and compatibility issues with quantized parameters, " - "but it usually offers faster inference speed and, in some cases, lower memory usage. " - "The at_runtime mode, on the other hand, is exactly the opposite.", - on_lora_apply_mode_arg}, - {"", - "--vae-tile-size", - "tile size for vae tiling, format [X]x[Y] (default: 32x32)", - on_tile_size_arg}, - {"", - "--vae-relative-tile-size", - "relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1 (overrides --vae-tile-size)", - on_relative_tile_size_arg}, - }; - - return options; - } - - void build_embedding_map() { - static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; - - if (!fs::exists(embedding_dir) || !fs::is_directory(embedding_dir)) { - return; - } - - for (auto& p : fs::directory_iterator(embedding_dir)) { - if (!p.is_regular_file()) - continue; - - auto path = p.path(); - std::string ext = path.extension().string(); - - bool valid = false; - for (auto& e : valid_ext) { - if (ext == e) { - valid = true; - break; - } - } - if (!valid) - continue; - - std::string key = path.stem().string(); - std::string value = path.string(); - - embedding_map[key] = value; - } - } - - bool process_and_check(SDMode mode) { - if (mode != UPSCALE && model_path.length() == 0 && diffusion_model_path.length() == 0) { - fprintf(stderr, "error: the following arguments are required: model_path/diffusion_model\n"); - return false; - } - - if (mode == UPSCALE) { - if (esrgan_path.length() == 0) { - fprintf(stderr, "error: upscale mode needs an upscaler model (--upscale-model)\n"); - return false; - } - } - - if (n_threads <= 0) { - n_threads = sd_get_num_physical_cores(); - } - - build_embedding_map(); - - return true; - } - - std::string to_string() const { - std::ostringstream emb_ss; - emb_ss << "{\n"; - for (auto it = embedding_map.begin(); it != embedding_map.end(); ++it) { - emb_ss << " \"" << it->first << "\": \"" << it->second << "\""; - if (std::next(it) != embedding_map.end()) { - emb_ss << ","; - } - emb_ss << "\n"; - } - emb_ss << " }"; - - std::string embeddings_str = emb_ss.str(); - std::ostringstream oss; - oss << "SDContextParams {\n" - << " n_threads: " << n_threads << ",\n" - << " model_path: \"" << model_path << "\",\n" - << " clip_l_path: \"" << clip_l_path << "\",\n" - << " clip_g_path: \"" << clip_g_path << "\",\n" - << " clip_vision_path: \"" << clip_vision_path << "\",\n" - << " t5xxl_path: \"" << t5xxl_path << "\",\n" - << " llm_path: \"" << llm_path << "\",\n" - << " llm_vision_path: \"" << llm_vision_path << "\",\n" - << " diffusion_model_path: \"" << diffusion_model_path << "\",\n" - << " high_noise_diffusion_model_path: \"" << high_noise_diffusion_model_path << "\",\n" - << " vae_path: \"" << vae_path << "\",\n" - << " taesd_path: \"" << taesd_path << "\",\n" - << " esrgan_path: \"" << esrgan_path << "\",\n" - << " control_net_path: \"" << control_net_path << "\",\n" - << " embedding_dir: \"" << embedding_dir << "\",\n" - << " embeddings: " << embeddings_str << "\n" - << " wtype: " << sd_type_name(wtype) << ",\n" - << " tensor_type_rules: \"" << tensor_type_rules << "\",\n" - << " lora_model_dir: \"" << lora_model_dir << "\",\n" - << " photo_maker_path: \"" << photo_maker_path << "\",\n" - << " rng_type: " << sd_rng_type_name(rng_type) << ",\n" - << " sampler_rng_type: " << sd_rng_type_name(sampler_rng_type) << ",\n" - << " flow_shift: " << (std::isinf(flow_shift) ? "INF" : std::to_string(flow_shift)) << "\n" - << " offload_params_to_cpu: " << (offload_params_to_cpu ? "true" : "false") << ",\n" - << " control_net_cpu: " << (control_net_cpu ? "true" : "false") << ",\n" - << " clip_on_cpu: " << (clip_on_cpu ? "true" : "false") << ",\n" - << " vae_on_cpu: " << (vae_on_cpu ? "true" : "false") << ",\n" - << " diffusion_flash_attn: " << (diffusion_flash_attn ? "true" : "false") << ",\n" - << " diffusion_conv_direct: " << (diffusion_conv_direct ? "true" : "false") << ",\n" - << " vae_conv_direct: " << (vae_conv_direct ? "true" : "false") << ",\n" - << " chroma_use_dit_mask: " << (chroma_use_dit_mask ? "true" : "false") << ",\n" - << " chroma_use_t5_mask: " << (chroma_use_t5_mask ? "true" : "false") << ",\n" - << " chroma_t5_mask_pad: " << chroma_t5_mask_pad << ",\n" - << " prediction: " << sd_prediction_name(prediction) << ",\n" - << " lora_apply_mode: " << sd_lora_apply_mode_name(lora_apply_mode) << ",\n" - << " vae_tiling_params: { " - << vae_tiling_params.enabled << ", " - << vae_tiling_params.tile_size_x << ", " - << vae_tiling_params.tile_size_y << ", " - << vae_tiling_params.target_overlap << ", " - << vae_tiling_params.rel_size_x << ", " - << vae_tiling_params.rel_size_y << " },\n" - << " force_sdxl_vae_conv_scale: " << (force_sdxl_vae_conv_scale ? "true" : "false") << "\n" - << "}"; - return oss.str(); - } - - sd_ctx_params_t to_sd_ctx_params_t(bool vae_decode_only, bool free_params_immediately, bool taesd_preview) { - embedding_vec.clear(); - embedding_vec.reserve(embedding_map.size()); - for (const auto& kv : embedding_map) { - sd_embedding_t item; - item.name = kv.first.c_str(); - item.path = kv.second.c_str(); - embedding_vec.emplace_back(item); - } - - sd_ctx_params_t sd_ctx_params = { - model_path.c_str(), - clip_l_path.c_str(), - clip_g_path.c_str(), - clip_vision_path.c_str(), - t5xxl_path.c_str(), - llm_path.c_str(), - llm_vision_path.c_str(), - diffusion_model_path.c_str(), - high_noise_diffusion_model_path.c_str(), - vae_path.c_str(), - taesd_path.c_str(), - control_net_path.c_str(), - lora_model_dir.c_str(), - embedding_vec.data(), - static_cast(embedding_vec.size()), - photo_maker_path.c_str(), - tensor_type_rules.c_str(), - vae_decode_only, - free_params_immediately, - n_threads, - wtype, - rng_type, - sampler_rng_type, - prediction, - lora_apply_mode, - offload_params_to_cpu, - clip_on_cpu, - control_net_cpu, - vae_on_cpu, - diffusion_flash_attn, - taesd_preview, - diffusion_conv_direct, - vae_conv_direct, - force_sdxl_vae_conv_scale, - chroma_use_dit_mask, - chroma_use_t5_mask, - chroma_t5_mask_pad, - flow_shift, - }; - return sd_ctx_params; - } -}; - -template -static std::string vec_to_string(const std::vector& v) { - std::ostringstream oss; - oss << "["; - for (size_t i = 0; i < v.size(); i++) { - oss << v[i]; - if (i + 1 < v.size()) - oss << ", "; - } - oss << "]"; - return oss.str(); -} - -static std::string vec_str_to_string(const std::vector& v) { - std::ostringstream oss; - oss << "["; - for (size_t i = 0; i < v.size(); i++) { - oss << "\"" << v[i] << "\""; - if (i + 1 < v.size()) - oss << ", "; - } - oss << "]"; - return oss.str(); -} - -static bool is_absolute_path(const std::string& p) { -#ifdef _WIN32 - // Windows: C:/path or C:\path - return p.size() > 1 && std::isalpha(static_cast(p[0])) && p[1] == ':'; -#else - return !p.empty() && p[0] == '/'; -#endif -} - -struct SDGenerationParams { - std::string prompt; - std::string negative_prompt; - int clip_skip = -1; // <= 0 represents unspecified - int width = 512; - int height = 512; - int batch_count = 1; - std::string init_image_path; - std::string end_image_path; - std::string mask_image_path; - std::string control_image_path; - std::vector ref_image_paths; - std::string control_video_path; - bool auto_resize_ref_image = true; - bool increase_ref_index = false; - - std::vector skip_layers = {7, 8, 9}; - sd_sample_params_t sample_params; - - std::vector high_noise_skip_layers = {7, 8, 9}; - sd_sample_params_t high_noise_sample_params; - - std::string easycache_option; - sd_easycache_params_t easycache_params; - - float moe_boundary = 0.875f; - int video_frames = 1; - int fps = 16; - float vace_strength = 1.f; - - float strength = 0.75f; - float control_strength = 0.9f; - - int64_t seed = 42; - - // Photo Maker - std::string pm_id_images_dir; - std::string pm_id_embed_path; - float pm_style_strength = 20.f; - - int upscale_repeats = 1; - int upscale_tile_size = 128; - - std::map lora_map; - std::map high_noise_lora_map; - std::vector lora_vec; - - SDGenerationParams() { - sd_sample_params_init(&sample_params); - sd_sample_params_init(&high_noise_sample_params); - } - - ArgOptions get_options() { - ArgOptions options; - options.string_options = { - {"-p", - "--prompt", - "the prompt to render", - &prompt}, - {"-n", - "--negative-prompt", - "the negative prompt (default: \"\")", - &negative_prompt}, - {"-i", - "--init-img", - "path to the init image", - &init_image_path}, - {"", - "--end-img", - "path to the end image, required by flf2v", - &end_image_path}, - {"", - "--mask", - "path to the mask image", - &mask_image_path}, - {"", - "--control-image", - "path to control image, control net", - &control_image_path}, - {"", - "--control-video", - "path to control video frames, It must be a directory path. The video frames inside should be stored as images in " - "lexicographical (character) order. For example, if the control video path is `frames`, the directory contain images " - "such as 00.png, 01.png, ... etc.", - &control_video_path}, - {"", - "--pm-id-images-dir", - "path to PHOTOMAKER input id images dir", - &pm_id_images_dir}, - {"", - "--pm-id-embed-path", - "path to PHOTOMAKER v2 id embed", - &pm_id_embed_path}, - }; - - options.int_options = { - {"-H", - "--height", - "image height, in pixel space (default: 512)", - &height}, - {"-W", - "--width", - "image width, in pixel space (default: 512)", - &width}, - {"", - "--steps", - "number of sample steps (default: 20)", - &sample_params.sample_steps}, - {"", - "--high-noise-steps", - "(high noise) number of sample steps (default: -1 = auto)", - &high_noise_sample_params.sample_steps}, - {"", - "--clip-skip", - "ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). " - "<= 0 represents unspecified, will be 1 for SD1.x, 2 for SD2.x", - &clip_skip}, - {"-b", - "--batch-count", - "batch count", - &batch_count}, - {"", - "--video-frames", - "video frames (default: 1)", - &video_frames}, - {"", - "--fps", - "fps (default: 24)", - &fps}, - {"", - "--timestep-shift", - "shift timestep for NitroFusion models (default: 0). " - "recommended N for NitroSD-Realism around 250 and 500 for NitroSD-Vibrant", - &sample_params.shifted_timestep}, - {"", - "--upscale-repeats", - "Run the ESRGAN upscaler this many times (default: 1)", - &upscale_repeats}, - {"", - "--upscale-tile-size", - "tile size for ESRGAN upscaling (default: 128)", - &upscale_tile_size}, - }; - - options.float_options = { - {"", - "--cfg-scale", - "unconditional guidance scale: (default: 7.0)", - &sample_params.guidance.txt_cfg}, - {"", - "--img-cfg-scale", - "image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale)", - &sample_params.guidance.img_cfg}, - {"", - "--guidance", - "distilled guidance scale for models with guidance input (default: 3.5)", - &sample_params.guidance.distilled_guidance}, - {"", - "--slg-scale", - "skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5 medium", - &sample_params.guidance.slg.scale}, - {"", - "--skip-layer-start", - "SLG enabling point (default: 0.01)", - &sample_params.guidance.slg.layer_start}, - {"", - "--skip-layer-end", - "SLG disabling point (default: 0.2)", - &sample_params.guidance.slg.layer_end}, - {"", - "--eta", - "eta in DDIM, only for DDIM and TCD (default: 0)", - &sample_params.eta}, - {"", - "--high-noise-cfg-scale", - "(high noise) unconditional guidance scale: (default: 7.0)", - &high_noise_sample_params.guidance.txt_cfg}, - {"", - "--high-noise-img-cfg-scale", - "(high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale)", - &high_noise_sample_params.guidance.img_cfg}, - {"", - "--high-noise-guidance", - "(high noise) distilled guidance scale for models with guidance input (default: 3.5)", - &high_noise_sample_params.guidance.distilled_guidance}, - {"", - "--high-noise-slg-scale", - "(high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0)", - &high_noise_sample_params.guidance.slg.scale}, - {"", - "--high-noise-skip-layer-start", - "(high noise) SLG enabling point (default: 0.01)", - &high_noise_sample_params.guidance.slg.layer_start}, - {"", - "--high-noise-skip-layer-end", - "(high noise) SLG disabling point (default: 0.2)", - &high_noise_sample_params.guidance.slg.layer_end}, - {"", - "--high-noise-eta", - "(high noise) eta in DDIM, only for DDIM and TCD (default: 0)", - &high_noise_sample_params.eta}, - {"", - "--strength", - "strength for noising/unnoising (default: 0.75)", - &strength}, - {"", - "--pm-style-strength", - "", - &pm_style_strength}, - {"", - "--control-strength", - "strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image", - &control_strength}, - {"", - "--moe-boundary", - "timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1", - &moe_boundary}, - {"", - "--vace-strength", - "wan vace strength", - &vace_strength}, - }; - - options.bool_options = { - {"", - "--increase-ref-index", - "automatically increase the indices of references images based on the order they are listed (starting with 1).", - true, - &increase_ref_index}, - {"", - "--disable-auto-resize-ref-image", - "disable auto resize of ref images", - false, - &auto_resize_ref_image}, - }; - - auto on_seed_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - seed = std::stoll(argv[index]); - return 1; - }; - - auto on_sample_method_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - sample_params.sample_method = str_to_sample_method(arg); - if (sample_params.sample_method == SAMPLE_METHOD_COUNT) { - fprintf(stderr, "error: invalid sample method %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_high_noise_sample_method_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - high_noise_sample_params.sample_method = str_to_sample_method(arg); - if (high_noise_sample_params.sample_method == SAMPLE_METHOD_COUNT) { - fprintf(stderr, "error: invalid high noise sample method %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_scheduler_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - const char* arg = argv[index]; - sample_params.scheduler = str_to_scheduler(arg); - if (sample_params.scheduler == SCHEDULER_COUNT) { - fprintf(stderr, "error: invalid scheduler %s\n", - arg); - return -1; - } - return 1; - }; - - auto on_skip_layers_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - std::string layers_str = argv[index]; - if (layers_str[0] != '[' || layers_str[layers_str.size() - 1] != ']') { - return -1; - } - - layers_str = layers_str.substr(1, layers_str.size() - 2); - - std::regex regex("[, ]+"); - std::sregex_token_iterator iter(layers_str.begin(), layers_str.end(), regex, -1); - std::sregex_token_iterator end; - std::vector tokens(iter, end); - std::vector layers; - for (const auto& token : tokens) { - try { - layers.push_back(std::stoi(token)); - } catch (const std::invalid_argument&) { - return -1; - } - } - skip_layers = layers; - return 1; - }; - - auto on_high_noise_skip_layers_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - std::string layers_str = argv[index]; - if (layers_str[0] != '[' || layers_str[layers_str.size() - 1] != ']') { - return -1; - } - - layers_str = layers_str.substr(1, layers_str.size() - 2); - - std::regex regex("[, ]+"); - std::sregex_token_iterator iter(layers_str.begin(), layers_str.end(), regex, -1); - std::sregex_token_iterator end; - std::vector tokens(iter, end); - std::vector layers; - for (const auto& token : tokens) { - try { - layers.push_back(std::stoi(token)); - } catch (const std::invalid_argument&) { - return -1; - } - } - high_noise_skip_layers = layers; - return 1; - }; - - auto on_ref_image_arg = [&](int argc, const char** argv, int index) { - if (++index >= argc) { - return -1; - } - ref_image_paths.push_back(argv[index]); - return 1; - }; - - auto on_easycache_arg = [&](int argc, const char** argv, int index) { - const std::string default_values = "0.2,0.15,0.95"; - auto looks_like_value = [](const std::string& token) { - if (token.empty()) { - return false; - } - if (token[0] != '-') { - return true; - } - if (token.size() == 1) { - return false; - } - unsigned char next = static_cast(token[1]); - return std::isdigit(next) || token[1] == '.'; - }; - - std::string option_value; - int consumed = 0; - if (index + 1 < argc) { - std::string next_arg = argv[index + 1]; - if (looks_like_value(next_arg)) { - option_value = argv_to_utf8(index + 1, argv); - consumed = 1; - } - } - if (option_value.empty()) { - option_value = default_values; - } - easycache_option = option_value; - return consumed; - }; - - options.manual_options = { - {"-s", - "--seed", - "RNG seed (default: 42, use random seed for < 0)", - on_seed_arg}, - {"", - "--sampling-method", - "sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd] " - "(default: euler for Flux/SD3/Wan, euler_a otherwise)", - on_sample_method_arg}, - {"", - "--high-noise-sampling-method", - "(high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd]" - " default: euler for Flux/SD3/Wan, euler_a otherwise", - on_high_noise_sample_method_arg}, - {"", - "--scheduler", - "denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], default: discrete", - on_scheduler_arg}, - {"", - "--skip-layers", - "layers to skip for SLG steps (default: [7,8,9])", - on_skip_layers_arg}, - {"", - "--high-noise-skip-layers", - "(high noise) layers to skip for SLG steps (default: [7,8,9])", - on_high_noise_skip_layers_arg}, - {"-r", - "--ref-image", - "reference image for Flux Kontext models (can be used multiple times)", - on_ref_image_arg}, - {"", - "--easycache", - "enable EasyCache for DiT models with optional \"threshold,start_percent,end_percent\" (default: 0.2,0.15,0.95)", - on_easycache_arg}, - - }; - - return options; - } - - void extract_and_remove_lora(const std::string& lora_model_dir) { - static const std::regex re(R"(]+):([^>]+)>)"); - static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; - std::smatch m; - - std::string tmp = prompt; - - while (std::regex_search(tmp, m, re)) { - std::string raw_path = m[1].str(); - const std::string raw_mul = m[2].str(); - - float mul = 0.f; - try { - mul = std::stof(raw_mul); - } catch (...) { - tmp = m.suffix().str(); - prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); - continue; - } - - bool is_high_noise = false; - static const std::string prefix = "|high_noise|"; - if (raw_path.rfind(prefix, 0) == 0) { - raw_path.erase(0, prefix.size()); - is_high_noise = true; - } - - fs::path final_path; - if (is_absolute_path(raw_path)) { - final_path = raw_path; - } else { - final_path = fs::path(lora_model_dir) / raw_path; - } - if (!fs::exists(final_path)) { - bool found = false; - for (const auto& ext : valid_ext) { - fs::path try_path = final_path; - try_path += ext; - if (fs::exists(try_path)) { - final_path = try_path; - found = true; - break; - } - } - if (!found) { - printf("can not found lora %s\n", final_path.lexically_normal().string().c_str()); - tmp = m.suffix().str(); - prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); - continue; - } - } - - const std::string key = final_path.lexically_normal().string(); - - if (is_high_noise) - high_noise_lora_map[key] += mul; - else - lora_map[key] += mul; - - prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); - - tmp = m.suffix().str(); - } - - for (const auto& kv : lora_map) { - sd_lora_t item; - item.is_high_noise = false; - item.path = kv.first.c_str(); - item.multiplier = kv.second; - lora_vec.emplace_back(item); - } - - for (const auto& kv : high_noise_lora_map) { - sd_lora_t item; - item.is_high_noise = true; - item.path = kv.first.c_str(); - item.multiplier = kv.second; - lora_vec.emplace_back(item); - } - } - - bool process_and_check(SDMode mode, const std::string& lora_model_dir) { - if (width <= 0) { - fprintf(stderr, "error: the width must be greater than 0\n"); - return false; - } - - if (height <= 0) { - fprintf(stderr, "error: the height must be greater than 0\n"); - return false; - } - - if (sample_params.sample_steps <= 0) { - fprintf(stderr, "error: the sample_steps must be greater than 0\n"); - return false; - } - - if (high_noise_sample_params.sample_steps <= 0) { - high_noise_sample_params.sample_steps = -1; - } - - if (strength < 0.f || strength > 1.f) { - fprintf(stderr, "error: can only work with strength in [0.0, 1.0]\n"); - return false; - } - - if (!easycache_option.empty()) { - float values[3] = {0.0f, 0.0f, 0.0f}; - std::stringstream ss(easycache_option); - std::string token; - int idx = 0; - while (std::getline(ss, token, ',')) { - auto trim = [](std::string& s) { - const char* whitespace = " \t\r\n"; - auto start = s.find_first_not_of(whitespace); - if (start == std::string::npos) { - s.clear(); - return; - } - auto end = s.find_last_not_of(whitespace); - s = s.substr(start, end - start + 1); - }; - trim(token); - if (token.empty()) { - fprintf(stderr, "error: invalid easycache option '%s'\n", easycache_option.c_str()); - return false; - } - if (idx >= 3) { - fprintf(stderr, "error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); - return false; - } - try { - values[idx] = std::stof(token); - } catch (const std::exception&) { - fprintf(stderr, "error: invalid easycache value '%s'\n", token.c_str()); - return false; - } - idx++; - } - if (idx != 3) { - fprintf(stderr, "error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); - return false; - } - if (values[0] < 0.0f) { - fprintf(stderr, "error: easycache threshold must be non-negative\n"); - return false; - } - if (values[1] < 0.0f || values[1] >= 1.0f || values[2] <= 0.0f || values[2] > 1.0f || values[1] >= values[2]) { - fprintf(stderr, "error: easycache start/end percents must satisfy 0.0 <= start < end <= 1.0\n"); - return false; - } - easycache_params.enabled = true; - easycache_params.reuse_threshold = values[0]; - easycache_params.start_percent = values[1]; - easycache_params.end_percent = values[2]; - } else { - easycache_params.enabled = false; - } - - sample_params.guidance.slg.layers = skip_layers.data(); - sample_params.guidance.slg.layer_count = skip_layers.size(); - high_noise_sample_params.guidance.slg.layers = high_noise_skip_layers.data(); - high_noise_sample_params.guidance.slg.layer_count = high_noise_skip_layers.size(); - - if (mode == VID_GEN && video_frames <= 0) { - return false; - } - - if (mode == VID_GEN && fps <= 0) { - return false; - } - - if (sample_params.shifted_timestep < 0 || sample_params.shifted_timestep > 1000) { - return false; - } - - if (upscale_repeats < 1) { - return false; - } - - if (upscale_tile_size < 1) { - return false; - } - - if (mode == UPSCALE) { - if (init_image_path.length() == 0) { - fprintf(stderr, "error: upscale mode needs an init image (--init-img)\n"); - return false; - } - } - - if (seed < 0) { - srand((int)time(nullptr)); - seed = rand(); - } - - extract_and_remove_lora(lora_model_dir); - - return true; - } - - std::string to_string() const { - char* sample_params_str = sd_sample_params_to_str(&sample_params); - char* high_noise_sample_params_str = sd_sample_params_to_str(&high_noise_sample_params); - - std::ostringstream lora_ss; - lora_ss << "{\n"; - for (auto it = lora_map.begin(); it != lora_map.end(); ++it) { - lora_ss << " \"" << it->first << "\": \"" << it->second << "\""; - if (std::next(it) != lora_map.end()) { - lora_ss << ","; - } - lora_ss << "\n"; - } - lora_ss << " }"; - std::string loras_str = lora_ss.str(); - - lora_ss = std::ostringstream(); - ; - lora_ss << "{\n"; - for (auto it = high_noise_lora_map.begin(); it != high_noise_lora_map.end(); ++it) { - lora_ss << " \"" << it->first << "\": \"" << it->second << "\""; - if (std::next(it) != high_noise_lora_map.end()) { - lora_ss << ","; - } - lora_ss << "\n"; - } - lora_ss << " }"; - std::string high_noise_loras_str = lora_ss.str(); - - std::ostringstream oss; - oss << "SDGenerationParams {\n" - << " loras: \"" << loras_str << "\",\n" - << " high_noise_loras: \"" << high_noise_loras_str << "\",\n" - << " prompt: \"" << prompt << "\",\n" - << " negative_prompt: \"" << negative_prompt << "\",\n" - << " clip_skip: " << clip_skip << ",\n" - << " width: " << width << ",\n" - << " height: " << height << ",\n" - << " batch_count: " << batch_count << ",\n" - << " init_image_path: \"" << init_image_path << "\",\n" - << " end_image_path: \"" << end_image_path << "\",\n" - << " mask_image_path: \"" << mask_image_path << "\",\n" - << " control_image_path: \"" << control_image_path << "\",\n" - << " ref_image_paths: " << vec_str_to_string(ref_image_paths) << ",\n" - << " control_video_path: \"" << control_video_path << "\",\n" - << " auto_resize_ref_image: " << (auto_resize_ref_image ? "true" : "false") << ",\n" - << " increase_ref_index: " << (increase_ref_index ? "true" : "false") << ",\n" - << " pm_id_images_dir: \"" << pm_id_images_dir << "\",\n" - << " pm_id_embed_path: \"" << pm_id_embed_path << "\",\n" - << " pm_style_strength: " << pm_style_strength << ",\n" - << " skip_layers: " << vec_to_string(skip_layers) << ",\n" - << " sample_params: " << sample_params_str << ",\n" - << " high_noise_skip_layers: " << vec_to_string(high_noise_skip_layers) << ",\n" - << " high_noise_sample_params: " << high_noise_sample_params_str << ",\n" - << " easycache_option: \"" << easycache_option << "\",\n" - << " easycache: " - << (easycache_params.enabled ? "enabled" : "disabled") - << " (threshold=" << easycache_params.reuse_threshold - << ", start=" << easycache_params.start_percent - << ", end=" << easycache_params.end_percent << "),\n" - << " moe_boundary: " << moe_boundary << ",\n" - << " video_frames: " << video_frames << ",\n" - << " fps: " << fps << ",\n" - << " vace_strength: " << vace_strength << ",\n" - << " strength: " << strength << ",\n" - << " control_strength: " << control_strength << ",\n" - << " seed: " << seed << ",\n" - << " upscale_repeats: " << upscale_repeats << ",\n" - << " upscale_tile_size: " << upscale_tile_size << ",\n" - << "}"; - free(sample_params_str); - free(high_noise_sample_params_str); - return oss.str(); - } -}; - -static std::string version_string() { - return std::string("stable-diffusion.cpp version ") + sd_version() + ", commit " + sd_commit(); -} - void print_usage(int argc, const char* argv[], const std::vector& options_list) { std::cout << version_string() << "\n"; std::cout << "Usage: " << argv[0] << " [options]\n\n"; @@ -1872,94 +323,6 @@ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { fflush(out_stream); } -uint8_t* load_image(const char* image_path, int& width, int& height, int expected_width = 0, int expected_height = 0, int expected_channel = 3) { - int c = 0; - uint8_t* image_buffer = (uint8_t*)stbi_load(image_path, &width, &height, &c, expected_channel); - if (image_buffer == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", image_path); - return nullptr; - } - if (c < expected_channel) { - fprintf(stderr, - "the number of channels for the input image must be >= %d," - "but got %d channels, image_path = %s\n", - expected_channel, - c, - image_path); - free(image_buffer); - return nullptr; - } - if (width <= 0) { - fprintf(stderr, "error: the width of image must be greater than 0, image_path = %s\n", image_path); - free(image_buffer); - return nullptr; - } - if (height <= 0) { - fprintf(stderr, "error: the height of image must be greater than 0, image_path = %s\n", image_path); - free(image_buffer); - return nullptr; - } - - // Resize input image ... - if ((expected_width > 0 && expected_height > 0) && (height != expected_height || width != expected_width)) { - float dst_aspect = (float)expected_width / (float)expected_height; - float src_aspect = (float)width / (float)height; - - int crop_x = 0, crop_y = 0; - int crop_w = width, crop_h = height; - - if (src_aspect > dst_aspect) { - crop_w = (int)(height * dst_aspect); - crop_x = (width - crop_w) / 2; - } else if (src_aspect < dst_aspect) { - crop_h = (int)(width / dst_aspect); - crop_y = (height - crop_h) / 2; - } - - if (crop_x != 0 || crop_y != 0) { - printf("crop input image from %dx%d to %dx%d, image_path = %s\n", width, height, crop_w, crop_h, image_path); - uint8_t* cropped_image_buffer = (uint8_t*)malloc(crop_w * crop_h * expected_channel); - if (cropped_image_buffer == nullptr) { - fprintf(stderr, "error: allocate memory for crop\n"); - free(image_buffer); - return nullptr; - } - for (int row = 0; row < crop_h; row++) { - uint8_t* src = image_buffer + ((crop_y + row) * width + crop_x) * expected_channel; - uint8_t* dst = cropped_image_buffer + (row * crop_w) * expected_channel; - memcpy(dst, src, crop_w * expected_channel); - } - - width = crop_w; - height = crop_h; - free(image_buffer); - image_buffer = cropped_image_buffer; - } - - printf("resize input image from %dx%d to %dx%d\n", width, height, expected_width, expected_height); - int resized_height = expected_height; - int resized_width = expected_width; - - uint8_t* resized_image_buffer = (uint8_t*)malloc(resized_height * resized_width * expected_channel); - if (resized_image_buffer == nullptr) { - fprintf(stderr, "error: allocate memory for resize input image\n"); - free(image_buffer); - return nullptr; - } - stbir_resize(image_buffer, width, height, 0, - resized_image_buffer, resized_width, resized_height, 0, STBIR_TYPE_UINT8, - expected_channel, STBIR_ALPHA_CHANNEL_NONE, 0, - STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, - STBIR_FILTER_BOX, STBIR_FILTER_BOX, - STBIR_COLORSPACE_SRGB, nullptr); - width = resized_width; - height = resized_height; - free(image_buffer); - image_buffer = resized_image_buffer; - } - return image_buffer; -} - bool load_images_from_dir(const std::string dir, std::vector& images, int expected_width = 0, @@ -1994,7 +357,7 @@ bool load_images_from_dir(const std::string dir, } int width = 0; int height = 0; - uint8_t* image_buffer = load_image(path.c_str(), width, height, expected_width, expected_height); + uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height, expected_width, expected_height); if (image_buffer == nullptr) { fprintf(stderr, "load image from '%s' failed\n", path.c_str()); return false; @@ -2130,7 +493,7 @@ int main(int argc, const char* argv[]) { int width = 0; int height = 0; - init_image.data = load_image(gen_params.init_image_path.c_str(), width, height, gen_params.width, gen_params.height); + init_image.data = load_image_from_file(gen_params.init_image_path.c_str(), width, height, gen_params.width, gen_params.height); if (init_image.data == nullptr) { fprintf(stderr, "load image from '%s' failed\n", gen_params.init_image_path.c_str()); release_all_resources(); @@ -2143,7 +506,7 @@ int main(int argc, const char* argv[]) { int width = 0; int height = 0; - end_image.data = load_image(gen_params.end_image_path.c_str(), width, height, gen_params.width, gen_params.height); + end_image.data = load_image_from_file(gen_params.end_image_path.c_str(), width, height, gen_params.width, gen_params.height); if (end_image.data == nullptr) { fprintf(stderr, "load image from '%s' failed\n", gen_params.end_image_path.c_str()); release_all_resources(); @@ -2155,7 +518,7 @@ int main(int argc, const char* argv[]) { int c = 0; int width = 0; int height = 0; - mask_image.data = load_image(gen_params.mask_image_path.c_str(), width, height, gen_params.width, gen_params.height, 1); + mask_image.data = load_image_from_file(gen_params.mask_image_path.c_str(), width, height, gen_params.width, gen_params.height, 1); if (mask_image.data == nullptr) { fprintf(stderr, "load image from '%s' failed\n", gen_params.mask_image_path.c_str()); release_all_resources(); @@ -2174,7 +537,7 @@ int main(int argc, const char* argv[]) { if (gen_params.control_image_path.size() > 0) { int width = 0; int height = 0; - control_image.data = load_image(gen_params.control_image_path.c_str(), width, height, gen_params.width, gen_params.height); + control_image.data = load_image_from_file(gen_params.control_image_path.c_str(), width, height, gen_params.width, gen_params.height); if (control_image.data == nullptr) { fprintf(stderr, "load image from '%s' failed\n", gen_params.control_image_path.c_str()); release_all_resources(); @@ -2195,7 +558,7 @@ int main(int argc, const char* argv[]) { for (auto& path : gen_params.ref_image_paths) { int width = 0; int height = 0; - uint8_t* image_buffer = load_image(path.c_str(), width, height); + uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height); if (image_buffer == nullptr) { fprintf(stderr, "load image from '%s' failed\n", path.c_str()); release_all_resources(); diff --git a/examples/common/common.hpp b/examples/common/common.hpp new file mode 100644 index 000000000..e508aba34 --- /dev/null +++ b/examples/common/common.hpp @@ -0,0 +1,1752 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +using json = nlohmann::json; +namespace fs = std::filesystem; + +#if defined(_WIN32) +#define NOMINMAX +#include +#endif // _WIN32 + +#include "stable-diffusion.h" + +#define STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_STATIC +#include "stb_image.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define STB_IMAGE_WRITE_STATIC +#include "stb_image_write.h" + +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#define STB_IMAGE_RESIZE_STATIC +#include "stb_image_resize.h" + +#define SAFE_STR(s) ((s) ? (s) : "") +#define BOOL_STR(b) ((b) ? "true" : "false") + +const char* modes_str[] = { + "img_gen", + "vid_gen", + "convert", + "upscale", +}; +#define SD_ALL_MODES_STR "img_gen, vid_gen, convert, upscale" + +enum SDMode { + IMG_GEN, + VID_GEN, + CONVERT, + UPSCALE, + MODE_COUNT +}; + +#if defined(_WIN32) +static std::string utf16_to_utf8(const std::wstring& wstr) { + if (wstr.empty()) + return {}; + int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), + nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) + throw std::runtime_error("UTF-16 to UTF-8 conversion failed"); + + std::string utf8(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), + (char*)utf8.data(), size_needed, nullptr, nullptr); + return utf8; +} + +static std::string argv_to_utf8(int index, const char** argv) { + int argc; + wchar_t** argv_w = CommandLineToArgvW(GetCommandLineW(), &argc); + if (!argv_w) + throw std::runtime_error("Failed to parse command line"); + + std::string result; + if (index < argc) { + result = utf16_to_utf8(argv_w[index]); + } + LocalFree(argv_w); + return result; +} + +#else // Linux / macOS +static std::string argv_to_utf8(int index, const char** argv) { + return std::string(argv[index]); +} + +#endif + +struct StringOption { + std::string short_name; + std::string long_name; + std::string desc; + std::string* target; +}; + +struct IntOption { + std::string short_name; + std::string long_name; + std::string desc; + int* target; +}; + +struct FloatOption { + std::string short_name; + std::string long_name; + std::string desc; + float* target; +}; + +struct BoolOption { + std::string short_name; + std::string long_name; + std::string desc; + bool keep_true; + bool* target; +}; + +struct ManualOption { + std::string short_name; + std::string long_name; + std::string desc; + std::function cb; +}; + +struct ArgOptions { + std::vector string_options; + std::vector int_options; + std::vector float_options; + std::vector bool_options; + std::vector manual_options; + + static std::string wrap_text(const std::string& text, size_t width, size_t indent) { + std::ostringstream oss; + size_t line_len = 0; + size_t pos = 0; + + while (pos < text.size()) { + // Preserve manual newlines + if (text[pos] == '\n') { + oss << '\n' + << std::string(indent, ' '); + line_len = indent; + ++pos; + continue; + } + + // Add the character + oss << text[pos]; + ++line_len; + ++pos; + + // If the current line exceeds width, try to break at the last space + if (line_len >= width) { + std::string current = oss.str(); + size_t back = current.size(); + + // Find the last space (for a clean break) + while (back > 0 && current[back - 1] != ' ' && current[back - 1] != '\n') + --back; + + // If found a space to break on + if (back > 0 && current[back - 1] != '\n') { + std::string before = current.substr(0, back - 1); + std::string after = current.substr(back); + oss.str(""); + oss.clear(); + oss << before << "\n" + << std::string(indent, ' ') << after; + } else { + // If no space found, just break at width + oss << "\n" + << std::string(indent, ' '); + } + line_len = indent; + } + } + + return oss.str(); + } + + void print() const { + constexpr size_t max_line_width = 120; + + struct Entry { + std::string names; + std::string desc; + }; + std::vector entries; + + auto add_entry = [&](const std::string& s, const std::string& l, + const std::string& desc, const std::string& hint = "") { + std::ostringstream ss; + if (!s.empty()) + ss << s; + if (!s.empty() && !l.empty()) + ss << ", "; + if (!l.empty()) + ss << l; + if (!hint.empty()) + ss << " " << hint; + entries.push_back({ss.str(), desc}); + }; + + for (auto& o : string_options) + add_entry(o.short_name, o.long_name, o.desc, ""); + for (auto& o : int_options) + add_entry(o.short_name, o.long_name, o.desc, ""); + for (auto& o : float_options) + add_entry(o.short_name, o.long_name, o.desc, ""); + for (auto& o : bool_options) + add_entry(o.short_name, o.long_name, o.desc, ""); + for (auto& o : manual_options) + add_entry(o.short_name, o.long_name, o.desc); + + size_t max_name_width = 0; + for (auto& e : entries) + max_name_width = std::max(max_name_width, e.names.size()); + + for (auto& e : entries) { + size_t indent = 2 + max_name_width + 4; + size_t desc_width = (max_line_width > indent ? max_line_width - indent : 40); + std::string wrapped_desc = wrap_text(e.desc, max_line_width, indent); + std::cout << " " << std::left << std::setw(static_cast(max_name_width) + 4) + << e.names << wrapped_desc << "\n"; + } + } +}; + +static bool parse_options(int argc, const char** argv, const std::vector& options_list) { + bool invalid_arg = false; + std::string arg; + + auto match_and_apply = [&](auto& opts, auto&& apply_fn) -> bool { + for (auto& option : opts) { + if ((option.short_name.size() > 0 && arg == option.short_name) || + (option.long_name.size() > 0 && arg == option.long_name)) { + apply_fn(option); + return true; + } + } + return false; + }; + + for (int i = 1; i < argc; i++) { + arg = argv[i]; + bool found_arg = false; + + for (auto& options : options_list) { + if (match_and_apply(options.string_options, [&](auto& option) { + if (++i >= argc) { + invalid_arg = true; + return; + } + *option.target = argv_to_utf8(i, argv); + found_arg = true; + })) + break; + + if (match_and_apply(options.int_options, [&](auto& option) { + if (++i >= argc) { + invalid_arg = true; + return; + } + *option.target = std::stoi(argv[i]); + found_arg = true; + })) + break; + + if (match_and_apply(options.float_options, [&](auto& option) { + if (++i >= argc) { + invalid_arg = true; + return; + } + *option.target = std::stof(argv[i]); + found_arg = true; + })) + break; + + if (match_and_apply(options.bool_options, [&](auto& option) { + *option.target = option.keep_true ? true : false; + found_arg = true; + })) + break; + + if (match_and_apply(options.manual_options, [&](auto& option) { + int ret = option.cb(argc, argv, i); + if (ret < 0) { + invalid_arg = true; + return; + } + i += ret; + found_arg = true; + })) + break; + } + + if (invalid_arg) { + fprintf(stderr, "error: invalid parameter for argument: %s\n", arg.c_str()); + return false; + } + if (!found_arg) { + fprintf(stderr, "error: unknown argument: %s\n", arg.c_str()); + return false; + } + } + + return true; +} + +struct SDContextParams { + int n_threads = -1; + std::string model_path; + std::string clip_l_path; + std::string clip_g_path; + std::string clip_vision_path; + std::string t5xxl_path; + std::string llm_path; + std::string llm_vision_path; + std::string diffusion_model_path; + std::string high_noise_diffusion_model_path; + std::string vae_path; + std::string taesd_path; + std::string esrgan_path; + std::string control_net_path; + std::string embedding_dir; + std::string photo_maker_path; + sd_type_t wtype = SD_TYPE_COUNT; + std::string tensor_type_rules; + std::string lora_model_dir; + + std::map embedding_map; + std::vector embedding_vec; + + rng_type_t rng_type = CUDA_RNG; + rng_type_t sampler_rng_type = RNG_TYPE_COUNT; + bool offload_params_to_cpu = false; + bool control_net_cpu = false; + bool clip_on_cpu = false; + bool vae_on_cpu = false; + bool diffusion_flash_attn = false; + bool diffusion_conv_direct = false; + bool vae_conv_direct = false; + + bool chroma_use_dit_mask = true; + bool chroma_use_t5_mask = false; + int chroma_t5_mask_pad = 1; + + prediction_t prediction = PREDICTION_COUNT; + lora_apply_mode_t lora_apply_mode = LORA_APPLY_AUTO; + + sd_tiling_params_t vae_tiling_params = {false, 0, 0, 0.5f, 0.0f, 0.0f}; + bool force_sdxl_vae_conv_scale = false; + + float flow_shift = INFINITY; + + ArgOptions get_options() { + ArgOptions options; + options.string_options = { + {"-m", + "--model", + "path to full model", + &model_path}, + {"", + "--clip_l", + "path to the clip-l text encoder", &clip_l_path}, + {"", "--clip_g", + "path to the clip-g text encoder", + &clip_g_path}, + {"", + "--clip_vision", + "path to the clip-vision encoder", + &clip_vision_path}, + {"", + "--t5xxl", + "path to the t5xxl text encoder", + &t5xxl_path}, + {"", + "--llm", + "path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...)", + &llm_path}, + {"", + "--llm_vision", + "path to the llm vit", + &llm_vision_path}, + {"", + "--qwen2vl", + "alias of --llm. Deprecated.", + &llm_path}, + {"", + "--qwen2vl_vision", + "alias of --llm_vision. Deprecated.", + &llm_vision_path}, + {"", + "--diffusion-model", + "path to the standalone diffusion model", + &diffusion_model_path}, + {"", + "--high-noise-diffusion-model", + "path to the standalone high noise diffusion model", + &high_noise_diffusion_model_path}, + {"", + "--vae", + "path to standalone vae model", + &vae_path}, + {"", + "--taesd", + "path to taesd. Using Tiny AutoEncoder for fast decoding (low quality)", + &taesd_path}, + {"", + "--control-net", + "path to control net model", + &control_net_path}, + {"", + "--embd-dir", + "embeddings directory", + &embedding_dir}, + {"", + "--lora-model-dir", + "lora model directory", + &lora_model_dir}, + + {"", + "--tensor-type-rules", + "weight type per tensor pattern (example: \"^vae\\.=f16,model\\.=q8_0\")", + &tensor_type_rules}, + {"", + "--photo-maker", + "path to PHOTOMAKER model", + &photo_maker_path}, + {"", + "--upscale-model", + "path to esrgan model.", + &esrgan_path}, + }; + + options.int_options = { + {"-t", + "--threads", + "number of threads to use during computation (default: -1). " + "If threads <= 0, then threads will be set to the number of CPU physical cores", + &n_threads}, + {"", + "--chroma-t5-mask-pad", + "t5 mask pad size of chroma", + &chroma_t5_mask_pad}, + }; + + options.float_options = { + {"", + "--vae-tile-overlap", + "tile overlap for vae tiling, in fraction of tile size (default: 0.5)", + &vae_tiling_params.target_overlap}, + {"", + "--flow-shift", + "shift value for Flow models like SD3.x or WAN (default: auto)", + &flow_shift}, + }; + + options.bool_options = { + {"", + "--vae-tiling", + "process vae in tiles to reduce memory usage", + true, &vae_tiling_params.enabled}, + {"", + "--force-sdxl-vae-conv-scale", + "force use of conv scale on sdxl vae", + true, &force_sdxl_vae_conv_scale}, + {"", + "--offload-to-cpu", + "place the weights in RAM to save VRAM, and automatically load them into VRAM when needed", + true, &offload_params_to_cpu}, + {"", + "--control-net-cpu", + "keep controlnet in cpu (for low vram)", + true, &control_net_cpu}, + {"", + "--clip-on-cpu", + "keep clip in cpu (for low vram)", + true, &clip_on_cpu}, + {"", + "--vae-on-cpu", + "keep vae in cpu (for low vram)", + true, &vae_on_cpu}, + {"", + "--diffusion-fa", + "use flash attention in the diffusion model", + true, &diffusion_flash_attn}, + {"", + "--diffusion-conv-direct", + "use ggml_conv2d_direct in the diffusion model", + true, &diffusion_conv_direct}, + {"", + "--vae-conv-direct", + "use ggml_conv2d_direct in the vae model", + true, &vae_conv_direct}, + {"", + "--chroma-disable-dit-mask", + "disable dit mask for chroma", + false, &chroma_use_dit_mask}, + {"", + "--chroma-enable-t5-mask", + "enable t5 mask for chroma", + true, &chroma_use_t5_mask}, + }; + + auto on_type_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + wtype = str_to_sd_type(arg); + if (wtype == SD_TYPE_COUNT) { + fprintf(stderr, "error: invalid weight format %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_rng_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + rng_type = str_to_rng_type(arg); + if (rng_type == RNG_TYPE_COUNT) { + fprintf(stderr, "error: invalid rng type %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_sampler_rng_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + sampler_rng_type = str_to_rng_type(arg); + if (sampler_rng_type == RNG_TYPE_COUNT) { + fprintf(stderr, "error: invalid sampler rng type %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_prediction_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + prediction = str_to_prediction(arg); + if (prediction == PREDICTION_COUNT) { + fprintf(stderr, "error: invalid prediction type %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_lora_apply_mode_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + lora_apply_mode = str_to_lora_apply_mode(arg); + if (lora_apply_mode == LORA_APPLY_MODE_COUNT) { + fprintf(stderr, "error: invalid lora apply model %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_tile_size_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + std::string tile_size_str = argv[index]; + size_t x_pos = tile_size_str.find('x'); + try { + if (x_pos != std::string::npos) { + std::string tile_x_str = tile_size_str.substr(0, x_pos); + std::string tile_y_str = tile_size_str.substr(x_pos + 1); + vae_tiling_params.tile_size_x = std::stoi(tile_x_str); + vae_tiling_params.tile_size_y = std::stoi(tile_y_str); + } else { + vae_tiling_params.tile_size_x = vae_tiling_params.tile_size_y = std::stoi(tile_size_str); + } + } catch (const std::invalid_argument&) { + return -1; + } catch (const std::out_of_range&) { + return -1; + } + return 1; + }; + + auto on_relative_tile_size_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + std::string rel_size_str = argv[index]; + size_t x_pos = rel_size_str.find('x'); + try { + if (x_pos != std::string::npos) { + std::string rel_x_str = rel_size_str.substr(0, x_pos); + std::string rel_y_str = rel_size_str.substr(x_pos + 1); + vae_tiling_params.rel_size_x = std::stof(rel_x_str); + vae_tiling_params.rel_size_y = std::stof(rel_y_str); + } else { + vae_tiling_params.rel_size_x = vae_tiling_params.rel_size_y = std::stof(rel_size_str); + } + } catch (const std::invalid_argument&) { + return -1; + } catch (const std::out_of_range&) { + return -1; + } + return 1; + }; + + options.manual_options = { + {"", + "--type", + "weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). " + "If not specified, the default is the type of the weight file", + on_type_arg}, + {"", + "--rng", + "RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui)", + on_rng_arg}, + {"", + "--sampler-rng", + "sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng", + on_sampler_rng_arg}, + {"", + "--prediction", + "prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow]", + on_prediction_arg}, + {"", + "--lora-apply-mode", + "the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. " + "In auto mode, if the model weights contain any quantized parameters, the at_runtime mode will be used; otherwise, immediately will be used." + "The immediately mode may have precision and compatibility issues with quantized parameters, " + "but it usually offers faster inference speed and, in some cases, lower memory usage. " + "The at_runtime mode, on the other hand, is exactly the opposite.", + on_lora_apply_mode_arg}, + {"", + "--vae-tile-size", + "tile size for vae tiling, format [X]x[Y] (default: 32x32)", + on_tile_size_arg}, + {"", + "--vae-relative-tile-size", + "relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1 (overrides --vae-tile-size)", + on_relative_tile_size_arg}, + }; + + return options; + } + + void build_embedding_map() { + static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; + + if (!fs::exists(embedding_dir) || !fs::is_directory(embedding_dir)) { + return; + } + + for (auto& p : fs::directory_iterator(embedding_dir)) { + if (!p.is_regular_file()) + continue; + + auto path = p.path(); + std::string ext = path.extension().string(); + + bool valid = false; + for (auto& e : valid_ext) { + if (ext == e) { + valid = true; + break; + } + } + if (!valid) + continue; + + std::string key = path.stem().string(); + std::string value = path.string(); + + embedding_map[key] = value; + } + } + + bool process_and_check(SDMode mode) { + if (mode != UPSCALE && model_path.length() == 0 && diffusion_model_path.length() == 0) { + fprintf(stderr, "error: the following arguments are required: model_path/diffusion_model\n"); + return false; + } + + if (mode == UPSCALE) { + if (esrgan_path.length() == 0) { + fprintf(stderr, "error: upscale mode needs an upscaler model (--upscale-model)\n"); + return false; + } + } + + if (n_threads <= 0) { + n_threads = sd_get_num_physical_cores(); + } + + build_embedding_map(); + + return true; + } + + std::string to_string() const { + std::ostringstream emb_ss; + emb_ss << "{\n"; + for (auto it = embedding_map.begin(); it != embedding_map.end(); ++it) { + emb_ss << " \"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != embedding_map.end()) { + emb_ss << ","; + } + emb_ss << "\n"; + } + emb_ss << " }"; + + std::string embeddings_str = emb_ss.str(); + std::ostringstream oss; + oss << "SDContextParams {\n" + << " n_threads: " << n_threads << ",\n" + << " model_path: \"" << model_path << "\",\n" + << " clip_l_path: \"" << clip_l_path << "\",\n" + << " clip_g_path: \"" << clip_g_path << "\",\n" + << " clip_vision_path: \"" << clip_vision_path << "\",\n" + << " t5xxl_path: \"" << t5xxl_path << "\",\n" + << " llm_path: \"" << llm_path << "\",\n" + << " llm_vision_path: \"" << llm_vision_path << "\",\n" + << " diffusion_model_path: \"" << diffusion_model_path << "\",\n" + << " high_noise_diffusion_model_path: \"" << high_noise_diffusion_model_path << "\",\n" + << " vae_path: \"" << vae_path << "\",\n" + << " taesd_path: \"" << taesd_path << "\",\n" + << " esrgan_path: \"" << esrgan_path << "\",\n" + << " control_net_path: \"" << control_net_path << "\",\n" + << " embedding_dir: \"" << embedding_dir << "\",\n" + << " embeddings: " << embeddings_str << "\n" + << " wtype: " << sd_type_name(wtype) << ",\n" + << " tensor_type_rules: \"" << tensor_type_rules << "\",\n" + << " lora_model_dir: \"" << lora_model_dir << "\",\n" + << " photo_maker_path: \"" << photo_maker_path << "\",\n" + << " rng_type: " << sd_rng_type_name(rng_type) << ",\n" + << " sampler_rng_type: " << sd_rng_type_name(sampler_rng_type) << ",\n" + << " flow_shift: " << (std::isinf(flow_shift) ? "INF" : std::to_string(flow_shift)) << "\n" + << " offload_params_to_cpu: " << (offload_params_to_cpu ? "true" : "false") << ",\n" + << " control_net_cpu: " << (control_net_cpu ? "true" : "false") << ",\n" + << " clip_on_cpu: " << (clip_on_cpu ? "true" : "false") << ",\n" + << " vae_on_cpu: " << (vae_on_cpu ? "true" : "false") << ",\n" + << " diffusion_flash_attn: " << (diffusion_flash_attn ? "true" : "false") << ",\n" + << " diffusion_conv_direct: " << (diffusion_conv_direct ? "true" : "false") << ",\n" + << " vae_conv_direct: " << (vae_conv_direct ? "true" : "false") << ",\n" + << " chroma_use_dit_mask: " << (chroma_use_dit_mask ? "true" : "false") << ",\n" + << " chroma_use_t5_mask: " << (chroma_use_t5_mask ? "true" : "false") << ",\n" + << " chroma_t5_mask_pad: " << chroma_t5_mask_pad << ",\n" + << " prediction: " << sd_prediction_name(prediction) << ",\n" + << " lora_apply_mode: " << sd_lora_apply_mode_name(lora_apply_mode) << ",\n" + << " vae_tiling_params: { " + << vae_tiling_params.enabled << ", " + << vae_tiling_params.tile_size_x << ", " + << vae_tiling_params.tile_size_y << ", " + << vae_tiling_params.target_overlap << ", " + << vae_tiling_params.rel_size_x << ", " + << vae_tiling_params.rel_size_y << " },\n" + << " force_sdxl_vae_conv_scale: " << (force_sdxl_vae_conv_scale ? "true" : "false") << "\n" + << "}"; + return oss.str(); + } + + sd_ctx_params_t to_sd_ctx_params_t(bool vae_decode_only, bool free_params_immediately, bool taesd_preview) { + embedding_vec.clear(); + embedding_vec.reserve(embedding_map.size()); + for (const auto& kv : embedding_map) { + sd_embedding_t item; + item.name = kv.first.c_str(); + item.path = kv.second.c_str(); + embedding_vec.emplace_back(item); + } + + sd_ctx_params_t sd_ctx_params = { + model_path.c_str(), + clip_l_path.c_str(), + clip_g_path.c_str(), + clip_vision_path.c_str(), + t5xxl_path.c_str(), + llm_path.c_str(), + llm_vision_path.c_str(), + diffusion_model_path.c_str(), + high_noise_diffusion_model_path.c_str(), + vae_path.c_str(), + taesd_path.c_str(), + control_net_path.c_str(), + lora_model_dir.c_str(), + embedding_vec.data(), + static_cast(embedding_vec.size()), + photo_maker_path.c_str(), + tensor_type_rules.c_str(), + vae_decode_only, + free_params_immediately, + n_threads, + wtype, + rng_type, + sampler_rng_type, + prediction, + lora_apply_mode, + offload_params_to_cpu, + clip_on_cpu, + control_net_cpu, + vae_on_cpu, + diffusion_flash_attn, + taesd_preview, + diffusion_conv_direct, + vae_conv_direct, + force_sdxl_vae_conv_scale, + chroma_use_dit_mask, + chroma_use_t5_mask, + chroma_t5_mask_pad, + flow_shift, + }; + return sd_ctx_params; + } +}; + +template +static std::string vec_to_string(const std::vector& v) { + std::ostringstream oss; + oss << "["; + for (size_t i = 0; i < v.size(); i++) { + oss << v[i]; + if (i + 1 < v.size()) + oss << ", "; + } + oss << "]"; + return oss.str(); +} + +static std::string vec_str_to_string(const std::vector& v) { + std::ostringstream oss; + oss << "["; + for (size_t i = 0; i < v.size(); i++) { + oss << "\"" << v[i] << "\""; + if (i + 1 < v.size()) + oss << ", "; + } + oss << "]"; + return oss.str(); +} + +static bool is_absolute_path(const std::string& p) { +#ifdef _WIN32 + // Windows: C:/path or C:\path + return p.size() > 1 && std::isalpha(static_cast(p[0])) && p[1] == ':'; +#else + return !p.empty() && p[0] == '/'; +#endif +} + +struct SDGenerationParams { + std::string prompt; + std::string negative_prompt; + int clip_skip = -1; // <= 0 represents unspecified + int width = 512; + int height = 512; + int batch_count = 1; + std::string init_image_path; + std::string end_image_path; + std::string mask_image_path; + std::string control_image_path; + std::vector ref_image_paths; + std::string control_video_path; + bool auto_resize_ref_image = true; + bool increase_ref_index = false; + + std::vector skip_layers = {7, 8, 9}; + sd_sample_params_t sample_params; + + std::vector high_noise_skip_layers = {7, 8, 9}; + sd_sample_params_t high_noise_sample_params; + + std::string easycache_option; + sd_easycache_params_t easycache_params; + + float moe_boundary = 0.875f; + int video_frames = 1; + int fps = 16; + float vace_strength = 1.f; + + float strength = 0.75f; + float control_strength = 0.9f; + + int64_t seed = 42; + + // Photo Maker + std::string pm_id_images_dir; + std::string pm_id_embed_path; + float pm_style_strength = 20.f; + + int upscale_repeats = 1; + int upscale_tile_size = 128; + + std::map lora_map; + std::map high_noise_lora_map; + std::vector lora_vec; + + SDGenerationParams() { + sd_sample_params_init(&sample_params); + sd_sample_params_init(&high_noise_sample_params); + } + + ArgOptions get_options() { + ArgOptions options; + options.string_options = { + {"-p", + "--prompt", + "the prompt to render", + &prompt}, + {"-n", + "--negative-prompt", + "the negative prompt (default: \"\")", + &negative_prompt}, + {"-i", + "--init-img", + "path to the init image", + &init_image_path}, + {"", + "--end-img", + "path to the end image, required by flf2v", + &end_image_path}, + {"", + "--mask", + "path to the mask image", + &mask_image_path}, + {"", + "--control-image", + "path to control image, control net", + &control_image_path}, + {"", + "--control-video", + "path to control video frames, It must be a directory path. The video frames inside should be stored as images in " + "lexicographical (character) order. For example, if the control video path is `frames`, the directory contain images " + "such as 00.png, 01.png, ... etc.", + &control_video_path}, + {"", + "--pm-id-images-dir", + "path to PHOTOMAKER input id images dir", + &pm_id_images_dir}, + {"", + "--pm-id-embed-path", + "path to PHOTOMAKER v2 id embed", + &pm_id_embed_path}, + }; + + options.int_options = { + {"-H", + "--height", + "image height, in pixel space (default: 512)", + &height}, + {"-W", + "--width", + "image width, in pixel space (default: 512)", + &width}, + {"", + "--steps", + "number of sample steps (default: 20)", + &sample_params.sample_steps}, + {"", + "--high-noise-steps", + "(high noise) number of sample steps (default: -1 = auto)", + &high_noise_sample_params.sample_steps}, + {"", + "--clip-skip", + "ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). " + "<= 0 represents unspecified, will be 1 for SD1.x, 2 for SD2.x", + &clip_skip}, + {"-b", + "--batch-count", + "batch count", + &batch_count}, + {"", + "--video-frames", + "video frames (default: 1)", + &video_frames}, + {"", + "--fps", + "fps (default: 24)", + &fps}, + {"", + "--timestep-shift", + "shift timestep for NitroFusion models (default: 0). " + "recommended N for NitroSD-Realism around 250 and 500 for NitroSD-Vibrant", + &sample_params.shifted_timestep}, + {"", + "--upscale-repeats", + "Run the ESRGAN upscaler this many times (default: 1)", + &upscale_repeats}, + {"", + "--upscale-tile-size", + "tile size for ESRGAN upscaling (default: 128)", + &upscale_tile_size}, + }; + + options.float_options = { + {"", + "--cfg-scale", + "unconditional guidance scale: (default: 7.0)", + &sample_params.guidance.txt_cfg}, + {"", + "--img-cfg-scale", + "image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale)", + &sample_params.guidance.img_cfg}, + {"", + "--guidance", + "distilled guidance scale for models with guidance input (default: 3.5)", + &sample_params.guidance.distilled_guidance}, + {"", + "--slg-scale", + "skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5 medium", + &sample_params.guidance.slg.scale}, + {"", + "--skip-layer-start", + "SLG enabling point (default: 0.01)", + &sample_params.guidance.slg.layer_start}, + {"", + "--skip-layer-end", + "SLG disabling point (default: 0.2)", + &sample_params.guidance.slg.layer_end}, + {"", + "--eta", + "eta in DDIM, only for DDIM and TCD (default: 0)", + &sample_params.eta}, + {"", + "--high-noise-cfg-scale", + "(high noise) unconditional guidance scale: (default: 7.0)", + &high_noise_sample_params.guidance.txt_cfg}, + {"", + "--high-noise-img-cfg-scale", + "(high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale)", + &high_noise_sample_params.guidance.img_cfg}, + {"", + "--high-noise-guidance", + "(high noise) distilled guidance scale for models with guidance input (default: 3.5)", + &high_noise_sample_params.guidance.distilled_guidance}, + {"", + "--high-noise-slg-scale", + "(high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0)", + &high_noise_sample_params.guidance.slg.scale}, + {"", + "--high-noise-skip-layer-start", + "(high noise) SLG enabling point (default: 0.01)", + &high_noise_sample_params.guidance.slg.layer_start}, + {"", + "--high-noise-skip-layer-end", + "(high noise) SLG disabling point (default: 0.2)", + &high_noise_sample_params.guidance.slg.layer_end}, + {"", + "--high-noise-eta", + "(high noise) eta in DDIM, only for DDIM and TCD (default: 0)", + &high_noise_sample_params.eta}, + {"", + "--strength", + "strength for noising/unnoising (default: 0.75)", + &strength}, + {"", + "--pm-style-strength", + "", + &pm_style_strength}, + {"", + "--control-strength", + "strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image", + &control_strength}, + {"", + "--moe-boundary", + "timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1", + &moe_boundary}, + {"", + "--vace-strength", + "wan vace strength", + &vace_strength}, + }; + + options.bool_options = { + {"", + "--increase-ref-index", + "automatically increase the indices of references images based on the order they are listed (starting with 1).", + true, + &increase_ref_index}, + {"", + "--disable-auto-resize-ref-image", + "disable auto resize of ref images", + false, + &auto_resize_ref_image}, + }; + + auto on_seed_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + seed = std::stoll(argv[index]); + return 1; + }; + + auto on_sample_method_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + sample_params.sample_method = str_to_sample_method(arg); + if (sample_params.sample_method == SAMPLE_METHOD_COUNT) { + fprintf(stderr, "error: invalid sample method %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_high_noise_sample_method_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + high_noise_sample_params.sample_method = str_to_sample_method(arg); + if (high_noise_sample_params.sample_method == SAMPLE_METHOD_COUNT) { + fprintf(stderr, "error: invalid high noise sample method %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_scheduler_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + const char* arg = argv[index]; + sample_params.scheduler = str_to_scheduler(arg); + if (sample_params.scheduler == SCHEDULER_COUNT) { + fprintf(stderr, "error: invalid scheduler %s\n", + arg); + return -1; + } + return 1; + }; + + auto on_skip_layers_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + std::string layers_str = argv[index]; + if (layers_str[0] != '[' || layers_str[layers_str.size() - 1] != ']') { + return -1; + } + + layers_str = layers_str.substr(1, layers_str.size() - 2); + + std::regex regex("[, ]+"); + std::sregex_token_iterator iter(layers_str.begin(), layers_str.end(), regex, -1); + std::sregex_token_iterator end; + std::vector tokens(iter, end); + std::vector layers; + for (const auto& token : tokens) { + try { + layers.push_back(std::stoi(token)); + } catch (const std::invalid_argument&) { + return -1; + } + } + skip_layers = layers; + return 1; + }; + + auto on_high_noise_skip_layers_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + std::string layers_str = argv[index]; + if (layers_str[0] != '[' || layers_str[layers_str.size() - 1] != ']') { + return -1; + } + + layers_str = layers_str.substr(1, layers_str.size() - 2); + + std::regex regex("[, ]+"); + std::sregex_token_iterator iter(layers_str.begin(), layers_str.end(), regex, -1); + std::sregex_token_iterator end; + std::vector tokens(iter, end); + std::vector layers; + for (const auto& token : tokens) { + try { + layers.push_back(std::stoi(token)); + } catch (const std::invalid_argument&) { + return -1; + } + } + high_noise_skip_layers = layers; + return 1; + }; + + auto on_ref_image_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + ref_image_paths.push_back(argv[index]); + return 1; + }; + + auto on_easycache_arg = [&](int argc, const char** argv, int index) { + const std::string default_values = "0.2,0.15,0.95"; + auto looks_like_value = [](const std::string& token) { + if (token.empty()) { + return false; + } + if (token[0] != '-') { + return true; + } + if (token.size() == 1) { + return false; + } + unsigned char next = static_cast(token[1]); + return std::isdigit(next) || token[1] == '.'; + }; + + std::string option_value; + int consumed = 0; + if (index + 1 < argc) { + std::string next_arg = argv[index + 1]; + if (looks_like_value(next_arg)) { + option_value = argv_to_utf8(index + 1, argv); + consumed = 1; + } + } + if (option_value.empty()) { + option_value = default_values; + } + easycache_option = option_value; + return consumed; + }; + + options.manual_options = { + {"-s", + "--seed", + "RNG seed (default: 42, use random seed for < 0)", + on_seed_arg}, + {"", + "--sampling-method", + "sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd] " + "(default: euler for Flux/SD3/Wan, euler_a otherwise)", + on_sample_method_arg}, + {"", + "--high-noise-sampling-method", + "(high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd]" + " default: euler for Flux/SD3/Wan, euler_a otherwise", + on_high_noise_sample_method_arg}, + {"", + "--scheduler", + "denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], default: discrete", + on_scheduler_arg}, + {"", + "--skip-layers", + "layers to skip for SLG steps (default: [7,8,9])", + on_skip_layers_arg}, + {"", + "--high-noise-skip-layers", + "(high noise) layers to skip for SLG steps (default: [7,8,9])", + on_high_noise_skip_layers_arg}, + {"-r", + "--ref-image", + "reference image for Flux Kontext models (can be used multiple times)", + on_ref_image_arg}, + {"", + "--easycache", + "enable EasyCache for DiT models with optional \"threshold,start_percent,end_percent\" (default: 0.2,0.15,0.95)", + on_easycache_arg}, + + }; + + return options; + } + + bool from_json_str(const std::string& json_str) { + json j; + try { + j = json::parse(json_str); + } catch (...) { + fprintf(stderr, "json parse failed %s\n", json_str.c_str()); + return false; + } + + auto load_if_exists = [&](const char* key, auto& out) { + if (j.contains(key)) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + if (j[key].is_string()) + out = j[key]; + } else if constexpr (std::is_same_v || std::is_same_v) { + if (j[key].is_number_integer()) + out = j[key]; + } else if constexpr (std::is_same_v) { + if (j[key].is_number()) + out = j[key]; + } else if constexpr (std::is_same_v) { + if (j[key].is_boolean()) + out = j[key]; + } else if constexpr (std::is_same_v>) { + if (j[key].is_array()) + out = j[key].get>(); + } else if constexpr (std::is_same_v>) { + if (j[key].is_array()) + out = j[key].get>(); + } + } + }; + + load_if_exists("prompt", prompt); + load_if_exists("negative_prompt", negative_prompt); + load_if_exists("easycache_option", easycache_option); + + load_if_exists("clip_skip", clip_skip); + load_if_exists("width", width); + load_if_exists("height", height); + load_if_exists("batch_count", batch_count); + load_if_exists("video_frames", video_frames); + load_if_exists("fps", fps); + load_if_exists("upscale_repeats", upscale_repeats); + load_if_exists("seed", seed); + + load_if_exists("strength", strength); + load_if_exists("control_strength", control_strength); + load_if_exists("pm_style_strength", pm_style_strength); + load_if_exists("moe_boundary", moe_boundary); + load_if_exists("vace_strength", vace_strength); + + load_if_exists("auto_resize_ref_image", auto_resize_ref_image); + load_if_exists("increase_ref_index", increase_ref_index); + + load_if_exists("skip_layers", skip_layers); + load_if_exists("high_noise_skip_layers", high_noise_skip_layers); + + load_if_exists("cfg_scale", sample_params.guidance.txt_cfg); + load_if_exists("img_cfg_scale", sample_params.guidance.img_cfg); + load_if_exists("guidance", sample_params.guidance.distilled_guidance); + + return true; + } + + void extract_and_remove_lora(const std::string& lora_model_dir) { + static const std::regex re(R"(]+):([^>]+)>)"); + static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; + std::smatch m; + + std::string tmp = prompt; + + while (std::regex_search(tmp, m, re)) { + std::string raw_path = m[1].str(); + const std::string raw_mul = m[2].str(); + + float mul = 0.f; + try { + mul = std::stof(raw_mul); + } catch (...) { + tmp = m.suffix().str(); + prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); + continue; + } + + bool is_high_noise = false; + static const std::string prefix = "|high_noise|"; + if (raw_path.rfind(prefix, 0) == 0) { + raw_path.erase(0, prefix.size()); + is_high_noise = true; + } + + fs::path final_path; + if (is_absolute_path(raw_path)) { + final_path = raw_path; + } else { + final_path = fs::path(lora_model_dir) / raw_path; + } + if (!fs::exists(final_path)) { + bool found = false; + for (const auto& ext : valid_ext) { + fs::path try_path = final_path; + try_path += ext; + if (fs::exists(try_path)) { + final_path = try_path; + found = true; + break; + } + } + if (!found) { + printf("can not found lora %s\n", final_path.lexically_normal().string().c_str()); + tmp = m.suffix().str(); + prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); + continue; + } + } + + const std::string key = final_path.lexically_normal().string(); + + if (is_high_noise) + high_noise_lora_map[key] += mul; + else + lora_map[key] += mul; + + prompt = std::regex_replace(prompt, re, "", std::regex_constants::format_first_only); + + tmp = m.suffix().str(); + } + + for (const auto& kv : lora_map) { + sd_lora_t item; + item.is_high_noise = false; + item.path = kv.first.c_str(); + item.multiplier = kv.second; + lora_vec.emplace_back(item); + } + + for (const auto& kv : high_noise_lora_map) { + sd_lora_t item; + item.is_high_noise = true; + item.path = kv.first.c_str(); + item.multiplier = kv.second; + lora_vec.emplace_back(item); + } + } + + bool process_and_check(SDMode mode, const std::string& lora_model_dir) { + if (width <= 0) { + fprintf(stderr, "error: the width must be greater than 0\n"); + return false; + } + + if (height <= 0) { + fprintf(stderr, "error: the height must be greater than 0\n"); + return false; + } + + if (sample_params.sample_steps <= 0) { + fprintf(stderr, "error: the sample_steps must be greater than 0\n"); + return false; + } + + if (high_noise_sample_params.sample_steps <= 0) { + high_noise_sample_params.sample_steps = -1; + } + + if (strength < 0.f || strength > 1.f) { + fprintf(stderr, "error: can only work with strength in [0.0, 1.0]\n"); + return false; + } + + if (!easycache_option.empty()) { + float values[3] = {0.0f, 0.0f, 0.0f}; + std::stringstream ss(easycache_option); + std::string token; + int idx = 0; + while (std::getline(ss, token, ',')) { + auto trim = [](std::string& s) { + const char* whitespace = " \t\r\n"; + auto start = s.find_first_not_of(whitespace); + if (start == std::string::npos) { + s.clear(); + return; + } + auto end = s.find_last_not_of(whitespace); + s = s.substr(start, end - start + 1); + }; + trim(token); + if (token.empty()) { + fprintf(stderr, "error: invalid easycache option '%s'\n", easycache_option.c_str()); + return false; + } + if (idx >= 3) { + fprintf(stderr, "error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); + return false; + } + try { + values[idx] = std::stof(token); + } catch (const std::exception&) { + fprintf(stderr, "error: invalid easycache value '%s'\n", token.c_str()); + return false; + } + idx++; + } + if (idx != 3) { + fprintf(stderr, "error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); + return false; + } + if (values[0] < 0.0f) { + fprintf(stderr, "error: easycache threshold must be non-negative\n"); + return false; + } + if (values[1] < 0.0f || values[1] >= 1.0f || values[2] <= 0.0f || values[2] > 1.0f || values[1] >= values[2]) { + fprintf(stderr, "error: easycache start/end percents must satisfy 0.0 <= start < end <= 1.0\n"); + return false; + } + easycache_params.enabled = true; + easycache_params.reuse_threshold = values[0]; + easycache_params.start_percent = values[1]; + easycache_params.end_percent = values[2]; + } else { + easycache_params.enabled = false; + } + + sample_params.guidance.slg.layers = skip_layers.data(); + sample_params.guidance.slg.layer_count = skip_layers.size(); + high_noise_sample_params.guidance.slg.layers = high_noise_skip_layers.data(); + high_noise_sample_params.guidance.slg.layer_count = high_noise_skip_layers.size(); + + if (mode == VID_GEN && video_frames <= 0) { + return false; + } + + if (mode == VID_GEN && fps <= 0) { + return false; + } + + if (sample_params.shifted_timestep < 0 || sample_params.shifted_timestep > 1000) { + return false; + } + + if (upscale_repeats < 1) { + return false; + } + + if (upscale_tile_size < 1) { + return false; + } + + if (mode == UPSCALE) { + if (init_image_path.length() == 0) { + fprintf(stderr, "error: upscale mode needs an init image (--init-img)\n"); + return false; + } + } + + if (seed < 0) { + srand((int)time(nullptr)); + seed = rand(); + } + + extract_and_remove_lora(lora_model_dir); + + return true; + } + + std::string to_string() const { + char* sample_params_str = sd_sample_params_to_str(&sample_params); + char* high_noise_sample_params_str = sd_sample_params_to_str(&high_noise_sample_params); + + std::ostringstream lora_ss; + lora_ss << "{\n"; + for (auto it = lora_map.begin(); it != lora_map.end(); ++it) { + lora_ss << " \"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != lora_map.end()) { + lora_ss << ","; + } + lora_ss << "\n"; + } + lora_ss << " }"; + std::string loras_str = lora_ss.str(); + + lora_ss = std::ostringstream(); + ; + lora_ss << "{\n"; + for (auto it = high_noise_lora_map.begin(); it != high_noise_lora_map.end(); ++it) { + lora_ss << " \"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != high_noise_lora_map.end()) { + lora_ss << ","; + } + lora_ss << "\n"; + } + lora_ss << " }"; + std::string high_noise_loras_str = lora_ss.str(); + + std::ostringstream oss; + oss << "SDGenerationParams {\n" + << " loras: \"" << loras_str << "\",\n" + << " high_noise_loras: \"" << high_noise_loras_str << "\",\n" + << " prompt: \"" << prompt << "\",\n" + << " negative_prompt: \"" << negative_prompt << "\",\n" + << " clip_skip: " << clip_skip << ",\n" + << " width: " << width << ",\n" + << " height: " << height << ",\n" + << " batch_count: " << batch_count << ",\n" + << " init_image_path: \"" << init_image_path << "\",\n" + << " end_image_path: \"" << end_image_path << "\",\n" + << " mask_image_path: \"" << mask_image_path << "\",\n" + << " control_image_path: \"" << control_image_path << "\",\n" + << " ref_image_paths: " << vec_str_to_string(ref_image_paths) << ",\n" + << " control_video_path: \"" << control_video_path << "\",\n" + << " auto_resize_ref_image: " << (auto_resize_ref_image ? "true" : "false") << ",\n" + << " increase_ref_index: " << (increase_ref_index ? "true" : "false") << ",\n" + << " pm_id_images_dir: \"" << pm_id_images_dir << "\",\n" + << " pm_id_embed_path: \"" << pm_id_embed_path << "\",\n" + << " pm_style_strength: " << pm_style_strength << ",\n" + << " skip_layers: " << vec_to_string(skip_layers) << ",\n" + << " sample_params: " << sample_params_str << ",\n" + << " high_noise_skip_layers: " << vec_to_string(high_noise_skip_layers) << ",\n" + << " high_noise_sample_params: " << high_noise_sample_params_str << ",\n" + << " easycache_option: \"" << easycache_option << "\",\n" + << " easycache: " + << (easycache_params.enabled ? "enabled" : "disabled") + << " (threshold=" << easycache_params.reuse_threshold + << ", start=" << easycache_params.start_percent + << ", end=" << easycache_params.end_percent << "),\n" + << " moe_boundary: " << moe_boundary << ",\n" + << " video_frames: " << video_frames << ",\n" + << " fps: " << fps << ",\n" + << " vace_strength: " << vace_strength << ",\n" + << " strength: " << strength << ",\n" + << " control_strength: " << control_strength << ",\n" + << " seed: " << seed << ",\n" + << " upscale_repeats: " << upscale_repeats << ",\n" + << " upscale_tile_size: " << upscale_tile_size << ",\n" + << "}"; + free(sample_params_str); + free(high_noise_sample_params_str); + return oss.str(); + } +}; + +static std::string version_string() { + return std::string("stable-diffusion.cpp version ") + sd_version() + ", commit " + sd_commit(); +} + +uint8_t* load_image_common(bool from_memory, + const char* image_path_or_bytes, + int len, + int& width, + int& height, + int expected_width = 0, + int expected_height = 0, + int expected_channel = 3) { + int c = 0; + const char* image_path; + uint8_t* image_buffer = nullptr; + if (from_memory) { + image_path = "memory"; + image_buffer = (uint8_t*)stbi_load_from_memory((const stbi_uc*)image_path_or_bytes, len, &width, &height, &c, expected_channel); + } else { + image_path = image_path_or_bytes; + image_buffer = (uint8_t*)stbi_load(image_path_or_bytes, &width, &height, &c, expected_channel); + } + if (image_buffer == nullptr) { + fprintf(stderr, "load image from '%s' failed\n", image_path); + return nullptr; + } + if (c < expected_channel) { + fprintf(stderr, + "the number of channels for the input image must be >= %d," + "but got %d channels, image_path = %s\n", + expected_channel, + c, + image_path); + free(image_buffer); + return nullptr; + } + if (width <= 0) { + fprintf(stderr, "error: the width of image must be greater than 0, image_path = %s\n", image_path); + free(image_buffer); + return nullptr; + } + if (height <= 0) { + fprintf(stderr, "error: the height of image must be greater than 0, image_path = %s\n", image_path); + free(image_buffer); + return nullptr; + } + + // Resize input image ... + if ((expected_width > 0 && expected_height > 0) && (height != expected_height || width != expected_width)) { + float dst_aspect = (float)expected_width / (float)expected_height; + float src_aspect = (float)width / (float)height; + + int crop_x = 0, crop_y = 0; + int crop_w = width, crop_h = height; + + if (src_aspect > dst_aspect) { + crop_w = (int)(height * dst_aspect); + crop_x = (width - crop_w) / 2; + } else if (src_aspect < dst_aspect) { + crop_h = (int)(width / dst_aspect); + crop_y = (height - crop_h) / 2; + } + + if (crop_x != 0 || crop_y != 0) { + printf("crop input image from %dx%d to %dx%d, image_path = %s\n", width, height, crop_w, crop_h, image_path); + uint8_t* cropped_image_buffer = (uint8_t*)malloc(crop_w * crop_h * expected_channel); + if (cropped_image_buffer == nullptr) { + fprintf(stderr, "error: allocate memory for crop\n"); + free(image_buffer); + return nullptr; + } + for (int row = 0; row < crop_h; row++) { + uint8_t* src = image_buffer + ((crop_y + row) * width + crop_x) * expected_channel; + uint8_t* dst = cropped_image_buffer + (row * crop_w) * expected_channel; + memcpy(dst, src, crop_w * expected_channel); + } + + width = crop_w; + height = crop_h; + free(image_buffer); + image_buffer = cropped_image_buffer; + } + + printf("resize input image from %dx%d to %dx%d\n", width, height, expected_width, expected_height); + int resized_height = expected_height; + int resized_width = expected_width; + + uint8_t* resized_image_buffer = (uint8_t*)malloc(resized_height * resized_width * expected_channel); + if (resized_image_buffer == nullptr) { + fprintf(stderr, "error: allocate memory for resize input image\n"); + free(image_buffer); + return nullptr; + } + stbir_resize(image_buffer, width, height, 0, + resized_image_buffer, resized_width, resized_height, 0, STBIR_TYPE_UINT8, + expected_channel, STBIR_ALPHA_CHANNEL_NONE, 0, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, + STBIR_FILTER_BOX, STBIR_FILTER_BOX, + STBIR_COLORSPACE_SRGB, nullptr); + width = resized_width; + height = resized_height; + free(image_buffer); + image_buffer = resized_image_buffer; + } + return image_buffer; +} + +uint8_t* load_image_from_file(const char* image_path, + int& width, + int& height, + int expected_width = 0, + int expected_height = 0, + int expected_channel = 3) { + return load_image_common(false, image_path, 0, width, height, expected_width, expected_height, expected_channel); +} + +uint8_t* load_image_from_memory(const char* image_bytes, + int len, + int& width, + int& height, + int expected_width = 0, + int expected_height = 0, + int expected_channel = 3) { + return load_image_common(true, image_bytes, len, width, height, expected_width, expected_height, expected_channel); +} diff --git a/examples/server/CMakeLists.txt b/examples/server/CMakeLists.txt new file mode 100644 index 000000000..d19126080 --- /dev/null +++ b/examples/server/CMakeLists.txt @@ -0,0 +1,6 @@ +set(TARGET sd-server) + +add_executable(${TARGET} main.cpp) +install(TARGETS ${TARGET} RUNTIME) +target_link_libraries(${TARGET} PRIVATE stable-diffusion ${CMAKE_THREAD_LIBS_INIT}) +target_compile_features(${TARGET} PUBLIC c_std_11 cxx_std_17) \ No newline at end of file diff --git a/examples/server/README.md b/examples/server/README.md new file mode 100644 index 000000000..533081af6 --- /dev/null +++ b/examples/server/README.md @@ -0,0 +1,63 @@ +# Run + +``` +usage: ./bin/sd-server [options] + +Svr Options: + -l, --listen-ip server listen ip (default: 127.0.0.1) + --listen-port server listen port (default: 1234) + -v, --verbose print extra info + --color colors the logging tags according to level + -h, --help show this help message and exit + +Context Options: + -m, --model path to full model + --clip_l path to the clip-l text encoder + --clip_g path to the clip-g text encoder + --clip_vision path to the clip-vision encoder + --t5xxl path to the t5xxl text encoder + --llm path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...) + --llm_vision path to the llm vit + --qwen2vl alias of --llm. Deprecated. + --qwen2vl_vision alias of --llm_vision. Deprecated. + --diffusion-model path to the standalone diffusion model + --high-noise-diffusion-model path to the standalone high noise diffusion model + --vae path to standalone vae model + --taesd path to taesd. Using Tiny AutoEncoder for fast decoding (low quality) + --control-net path to control net model + --embd-dir embeddings directory + --lora-model-dir lora model directory + --tensor-type-rules weight type per tensor pattern (example: "^vae\.=f16,model\.=q8_0") + --photo-maker path to PHOTOMAKER model + --upscale-model path to esrgan model. + -t, --threads number of threads to use during computation (default: -1). If threads <= 0, then threads will be set to the number of + CPU physical cores + --chroma-t5-mask-pad t5 mask pad size of chroma + --vae-tile-overlap tile overlap for vae tiling, in fraction of tile size (default: 0.5) + --flow-shift shift value for Flow models like SD3.x or WAN (default: auto) + --vae-tiling process vae in tiles to reduce memory usage + --force-sdxl-vae-conv-scale force use of conv scale on sdxl vae + --offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM when needed + --control-net-cpu keep controlnet in cpu (for low vram) + --clip-on-cpu keep clip in cpu (for low vram) + --vae-on-cpu keep vae in cpu (for low vram) + --diffusion-fa use flash attention in the diffusion model + --diffusion-conv-direct use ggml_conv2d_direct in the diffusion model + --vae-conv-direct use ggml_conv2d_direct in the vae model + --chroma-disable-dit-mask disable dit mask for chroma + --chroma-enable-t5-mask enable t5 mask for chroma + --type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the + type of the weight file + --rng RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui) + --sampler-rng sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng + --prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow] + --lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. In auto mode, if the model weights + contain any quantized parameters, the at_runtime mode will be used; otherwise, + immediately will be used.The immediately mode may have precision and + compatibility issues with quantized parameters, but it usually offers faster inference + speed and, in some cases, lower memory usage. The at_runtime mode, on the + other hand, is exactly the opposite. + --vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32) + --vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1 + (overrides --vae-tile-size) +``` \ No newline at end of file diff --git a/examples/server/main.cpp b/examples/server/main.cpp new file mode 100644 index 000000000..3bfaa36d4 --- /dev/null +++ b/examples/server/main.cpp @@ -0,0 +1,715 @@ +// main.cpp +#include +#include +#include +#include +#include +#include +#include +#include + +#include "httplib.h" +#include "stable-diffusion.h" + +#include "common/common.hpp" + +namespace fs = std::filesystem; + +// ----------------------- helpers ----------------------- +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +std::string base64_encode(const std::vector& bytes) { + std::string ret; + int val = 0, valb = -6; + for (uint8_t c : bytes) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + ret.push_back(base64_chars[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) + ret.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]); + while (ret.size() % 4) + ret.push_back('='); + return ret; +} + +inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::vector base64_decode(const std::string& encoded_string) { + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + uint8_t char_array_4[4], char_array_3[3]; + std::vector ret; + + while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; + in_++; + if (i == 4) { + for (i = 0; i < 4; i++) + char_array_4[i] = static_cast(base64_chars.find(char_array_4[i])); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; i < 3; i++) + ret.push_back(char_array_3[i]); + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) + char_array_4[j] = 0; + + for (j = 0; j < 4; j++) + char_array_4[j] = static_cast(base64_chars.find(char_array_4[j])); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; j < i - 1; j++) + ret.push_back(char_array_3[j]); + } + + return ret; +} + +std::string iso_timestamp_now() { + using namespace std::chrono; + auto now = system_clock::now(); + std::time_t t = system_clock::to_time_t(now); + std::tm tm{}; +#ifdef _MSC_VER + gmtime_s(&tm, &t); +#else + gmtime_r(&t, &tm); +#endif + std::ostringstream oss; + oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ"); + return oss.str(); +} + +struct SDSvrParams { + std::string listen_ip = "127.0.0.1"; + int listen_port = 1234; + bool normal_exit = false; + bool verbose = false; + bool color = false; + + ArgOptions get_options() { + ArgOptions options; + + options.string_options = { + {"-l", + "--listen-ip", + "server listen ip (default: 127.0.0.1)", + &listen_ip}}; + + options.int_options = { + {"", + "--listen-port", + "server listen port (default: 1234)", + &listen_port}, + }; + + options.bool_options = { + {"-v", + "--verbose", + "print extra info", + true, &verbose}, + {"", + "--color", + "colors the logging tags according to level", + true, &color}, + }; + + auto on_help_arg = [&](int argc, const char** argv, int index) { + normal_exit = true; + return -1; + }; + + options.manual_options = { + {"-h", + "--help", + "show this help message and exit", + on_help_arg}, + }; + return options; + }; + + bool process_and_check() { + if (listen_ip.empty()) { + fprintf(stderr, "error: the following arguments are required: listen_ip\n"); + return false; + } + + if (listen_port < 0 || listen_port > 65535) { + fprintf(stderr, "error: listen_port should be in the range [0, 65535]\n"); + return false; + } + return true; + } + + std::string to_string() const { + std::ostringstream oss; + oss << "SDSvrParams {\n" + << " listen_ip: " << listen_ip << ",\n" + << " listen_port: \"" << listen_port << "\",\n" + << "}"; + return oss.str(); + } +}; + +void print_usage(int argc, const char* argv[], const std::vector& options_list) { + std::cout << version_string() << "\n"; + std::cout << "Usage: " << argv[0] << " [options]\n\n"; + std::cout << "Svr Options:\n"; + options_list[0].print(); + std::cout << "\nContext Options:\n"; + options_list[1].print(); +} + +void parse_args(int argc, const char** argv, SDSvrParams& svr_params, SDContextParams& ctx_params) { + std::vector options_vec = {svr_params.get_options(), ctx_params.get_options()}; + + if (!parse_options(argc, argv, options_vec)) { + print_usage(argc, argv, options_vec); + exit(svr_params.normal_exit ? 0 : 1); + } + + if (!svr_params.process_and_check() || !ctx_params.process_and_check(IMG_GEN)) { + print_usage(argc, argv, options_vec); + exit(1); + } +} + +std::string extract_and_remove_sd_cpp_extra_args(std::string& text) { + std::regex re("(.*?)"); + std::smatch match; + + std::string extracted; + if (std::regex_search(text, match, re)) { + extracted = match[1].str(); + text = std::regex_replace(text, re, ""); + } + return extracted; +} + +enum class ImageFormat { JPEG, + PNG }; + +std::vector write_image_to_vector( + ImageFormat format, + const uint8_t* image, + int width, + int height, + int channels, + int quality = 90) { + std::vector buffer; + + auto write_func = [&buffer](void* context, void* data, int size) { + uint8_t* src = reinterpret_cast(data); + buffer.insert(buffer.end(), src, src + size); + }; + + struct ContextWrapper { + decltype(write_func)& func; + } ctx{write_func}; + + auto c_func = [](void* context, void* data, int size) { + auto* wrapper = reinterpret_cast(context); + wrapper->func(context, data, size); + }; + + int result = 0; + switch (format) { + case ImageFormat::JPEG: + result = stbi_write_jpg_to_func(c_func, &ctx, width, height, channels, image, quality); + break; + case ImageFormat::PNG: + result = stbi_write_png_to_func(c_func, &ctx, width, height, channels, image, width * channels); + break; + default: + throw std::runtime_error("invalid image format"); + } + + if (!result) { + throw std::runtime_error("write imgage to mem failed"); + } + + return buffer; +} + +/* Enables Printing the log level tag in color using ANSI escape codes */ +void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { + SDSvrParams* svr_params = (SDSvrParams*)data; + int tag_color; + const char* level_str; + FILE* out_stream = (level == SD_LOG_ERROR) ? stderr : stdout; + + if (!log || (!svr_params->verbose && level <= SD_LOG_DEBUG)) { + return; + } + + switch (level) { + case SD_LOG_DEBUG: + tag_color = 37; + level_str = "DEBUG"; + break; + case SD_LOG_INFO: + tag_color = 34; + level_str = "INFO"; + break; + case SD_LOG_WARN: + tag_color = 35; + level_str = "WARN"; + break; + case SD_LOG_ERROR: + tag_color = 31; + level_str = "ERROR"; + break; + default: /* Potential future-proofing */ + tag_color = 33; + level_str = "?????"; + break; + } + + if (svr_params->color == true) { + fprintf(out_stream, "\033[%d;1m[%-5s]\033[0m ", tag_color, level_str); + } else { + fprintf(out_stream, "[%-5s] ", level_str); + } + fputs(log, out_stream); + fflush(out_stream); +} + +int main(int argc, const char** argv) { + SDSvrParams svr_params; + SDContextParams ctx_params; + parse_args(argc, argv, svr_params, ctx_params); + + sd_set_log_callback(sd_log_cb, (void*)&svr_params); + + if (svr_params.verbose) { + printf("%s", sd_get_system_info()); + printf("%s\n", svr_params.to_string().c_str()); + printf("%s\n", ctx_params.to_string().c_str()); + } + + sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false, false, false); + sd_ctx_t* sd_ctx = new_sd_ctx(&sd_ctx_params); + + if (sd_ctx == nullptr) { + printf("new_sd_ctx_t failed\n"); + return 1; + } + + std::mutex sd_ctx_mutex; + + httplib::Server svr; + + // health + svr.Get("/", [&](const httplib::Request&, httplib::Response& res) { + res.set_content(R"({"ok":true,"service":"sd-cpp-http"})", "application/json"); + }); + + // models endpoint (minimal) + svr.Get("/v1/models", [&](const httplib::Request&, httplib::Response& res) { + json r; + r["data"] = json::array(); + r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}}); + res.set_content(r.dump(), "application/json"); + }); + + // core endpoint: /v1/images/generations + svr.Post("/v1/images/generations", [&](const httplib::Request& req, httplib::Response& res) { + try { + if (req.body.empty()) { + res.status = 400; + res.set_content(R"({"error":"empty body"})", "application/json"); + return; + } + + json j = json::parse(req.body); + std::string prompt = j.value("prompt", ""); + int n = std::max(1, j.value("n", 1)); + std::string size = j.value("size", ""); + std::string output_format = j.value("output_format", "png"); + int output_compression = j.value("output_compression", 100); + int width = 512; + int height = 512; + if (!size.empty()) { + auto pos = size.find('x'); + if (pos != std::string::npos) { + try { + width = std::stoi(size.substr(0, pos)); + height = std::stoi(size.substr(pos + 1)); + } catch (...) { + } + } + } + + if (prompt.empty()) { + res.status = 400; + res.set_content(R"({"error":"prompt required"})", "application/json"); + return; + } + + std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(prompt); + + if (output_format != "png" && output_format != "jpeg") { + res.status = 400; + res.set_content(R"({"error":"invalid output_format, must be one of [png, jpeg]"})", "application/json"); + return; + } + if (n <= 0) + n = 1; + if (n > 8) + n = 8; // safety + if (output_compression > 100) { + output_compression = 100; + } + if (output_compression < 0) { + output_compression = 0; + } + + json out; + out["created"] = iso_timestamp_now(); + out["data"] = json::array(); + out["output_format"] = output_format; + + SDGenerationParams gen_params; + gen_params.prompt = prompt; + gen_params.width = width; + gen_params.height = height; + gen_params.batch_count = n; + + if (!sd_cpp_extra_args_str.empty() && !gen_params.from_json_str(sd_cpp_extra_args_str)) { + res.status = 400; + res.set_content(R"({"error":"invalid sd_cpp_extra_args"})", "application/json"); + return; + } + + if (!gen_params.process_and_check(IMG_GEN, ctx_params.lora_model_dir)) { + res.status = 400; + res.set_content(R"({"error":"invalid params"})", "application/json"); + return; + } + + if (svr_params.verbose) { + printf("%s\n", gen_params.to_string().c_str()); + } + + sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; + sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; + sd_image_t mask_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 1, nullptr}; + std::vector pmid_images; + + sd_img_gen_params_t img_gen_params = { + gen_params.lora_vec.data(), + static_cast(gen_params.lora_vec.size()), + gen_params.prompt.c_str(), + gen_params.negative_prompt.c_str(), + gen_params.clip_skip, + init_image, + nullptr, + 0, + gen_params.auto_resize_ref_image, + gen_params.increase_ref_index, + mask_image, + gen_params.width, + gen_params.height, + gen_params.sample_params, + gen_params.strength, + gen_params.seed, + gen_params.batch_count, + control_image, + gen_params.control_strength, + { + pmid_images.data(), + (int)pmid_images.size(), + gen_params.pm_id_embed_path.c_str(), + gen_params.pm_style_strength, + }, // pm_params + ctx_params.vae_tiling_params, + gen_params.easycache_params, + }; + + sd_image_t* results = nullptr; + int num_results = 0; + + { + std::lock_guard lock(sd_ctx_mutex); + results = generate_image(sd_ctx, &img_gen_params); + num_results = gen_params.batch_count; + } + + for (int i = 0; i < num_results; i++) { + if (results[i].data == nullptr) { + continue; + } + auto image_bytes = write_image_to_vector(output_format == "jpeg" ? ImageFormat::JPEG : ImageFormat::PNG, + results[i].data, + results[i].width, + results[i].height, + results[i].channel, + output_compression); + if (image_bytes.empty()) { + printf("write image to mem failed\n"); + continue; + } + + // base64 encode + std::string b64 = base64_encode(image_bytes); + json item; + item["b64_json"] = b64; + out["data"].push_back(item); + } + + res.set_content(out.dump(), "application/json"); + res.status = 200; + + } catch (const std::exception& e) { + res.status = 500; + json err; + err["error"] = "server_error"; + err["message"] = e.what(); + res.set_content(err.dump(), "application/json"); + } + }); + + svr.Post("/v1/images/edits", [&](const httplib::Request& req, httplib::Response& res) { + try { + if (!req.is_multipart_form_data()) { + res.status = 400; + res.set_content(R"({"error":"Content-Type must be multipart/form-data"})", "application/json"); + return; + } + + std::string prompt = req.form.get_field("prompt"); + if (prompt.empty()) { + res.status = 400; + res.set_content(R"({"error":"prompt required"})", "application/json"); + return; + } + + std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(prompt); + + size_t image_count = req.form.get_file_count("image[]"); + if (image_count == 0) { + res.status = 400; + res.set_content(R"({"error":"at least one image[] required"})", "application/json"); + return; + } + + std::vector> images_bytes; + for (size_t i = 0; i < image_count; i++) { + auto file = req.form.get_file("image[]", i); + images_bytes.emplace_back(file.content.begin(), file.content.end()); + } + + std::vector mask_bytes; + if (req.form.has_field("mask")) { + auto file = req.form.get_file("mask"); + mask_bytes.assign(file.content.begin(), file.content.end()); + } + + int n = 1; + if (req.form.has_field("n")) { + try { + n = std::stoi(req.form.get_field("n")); + } catch (...) { + } + } + n = std::clamp(n, 1, 8); + + std::string size = req.form.get_field("size"); + int width = 512, height = 512; + if (!size.empty()) { + auto pos = size.find('x'); + if (pos != std::string::npos) { + try { + width = std::stoi(size.substr(0, pos)); + height = std::stoi(size.substr(pos + 1)); + } catch (...) { + } + } + } + + std::string output_format = "png"; + if (req.form.has_field("output_format")) + output_format = req.form.get_field("output_format"); + if (output_format != "png" && output_format != "jpeg") { + res.status = 400; + res.set_content(R"({"error":"invalid output_format, must be one of [png, jpeg]"})", "application/json"); + return; + } + + std::string output_compression_str = req.form.get_field("output_compression"); + int output_compression = 100; + try { + output_compression = std::stoi(output_compression_str); + } catch (...) { + } + if (output_compression > 100) { + output_compression = 100; + } + if (output_compression < 0) { + output_compression = 0; + } + + SDGenerationParams gen_params; + gen_params.prompt = prompt; + gen_params.width = width; + gen_params.height = height; + gen_params.batch_count = n; + + if (!sd_cpp_extra_args_str.empty() && !gen_params.from_json_str(sd_cpp_extra_args_str)) { + res.status = 400; + res.set_content(R"({"error":"invalid sd_cpp_extra_args"})", "application/json"); + return; + } + + if (svr_params.verbose) { + printf("%s\n", gen_params.to_string().c_str()); + } + + sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; + sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; + std::vector pmid_images; + + std::vector ref_images; + ref_images.reserve(images_bytes.size()); + for (auto& bytes : images_bytes) { + int img_w = width; + int img_h = height; + uint8_t* raw_pixels = load_image_from_memory( + reinterpret_cast(bytes.data()), + bytes.size(), + img_w, img_h, + width, height, 3); + + if (!raw_pixels) { + continue; + } + + sd_image_t img{(uint32_t)img_w, (uint32_t)img_h, 3, raw_pixels}; + ref_images.push_back(img); + } + + sd_image_t mask_image = {0}; + if (!mask_bytes.empty()) { + int mask_w = width; + int mask_h = height; + uint8_t* mask_raw = load_image_from_memory( + reinterpret_cast(mask_bytes.data()), + mask_bytes.size(), + mask_w, mask_h, + width, height, 1); + mask_image = {(uint32_t)mask_w, (uint32_t)mask_h, 1, mask_raw}; + } else { + mask_image.width = width; + mask_image.height = height; + mask_image.channel = 1; + mask_image.data = nullptr; + } + + sd_img_gen_params_t img_gen_params = { + gen_params.lora_vec.data(), + static_cast(gen_params.lora_vec.size()), + gen_params.prompt.c_str(), + gen_params.negative_prompt.c_str(), + gen_params.clip_skip, + init_image, + ref_images.data(), + (int)ref_images.size(), + gen_params.auto_resize_ref_image, + gen_params.increase_ref_index, + mask_image, + gen_params.width, + gen_params.height, + gen_params.sample_params, + gen_params.strength, + gen_params.seed, + gen_params.batch_count, + control_image, + gen_params.control_strength, + { + pmid_images.data(), + (int)pmid_images.size(), + gen_params.pm_id_embed_path.c_str(), + gen_params.pm_style_strength, + }, // pm_params + ctx_params.vae_tiling_params, + gen_params.easycache_params, + }; + + sd_image_t* results = nullptr; + int num_results = 0; + + { + std::lock_guard lock(sd_ctx_mutex); + results = generate_image(sd_ctx, &img_gen_params); + num_results = gen_params.batch_count; + } + + json out; + out["created"] = iso_timestamp_now(); + out["data"] = json::array(); + out["output_format"] = output_format; + + for (int i = 0; i < num_results; i++) { + if (results[i].data == nullptr) + continue; + auto image_bytes = write_image_to_vector(output_format == "jpeg" ? ImageFormat::JPEG : ImageFormat::PNG, + results[i].data, + results[i].width, + results[i].height, + results[i].channel, + output_compression); + std::string b64 = base64_encode(image_bytes); + json item; + item["b64_json"] = b64; + out["data"].push_back(item); + } + + res.set_content(out.dump(), "application/json"); + res.status = 200; + + if (init_image.data) { + stbi_image_free(init_image.data); + } + if (mask_image.data) { + stbi_image_free(mask_image.data); + } + for (auto ref_image : ref_images) { + stbi_image_free(ref_image.data); + } + } catch (const std::exception& e) { + res.status = 500; + json err; + err["error"] = "server_error"; + err["message"] = e.what(); + res.set_content(err.dump(), "application/json"); + } + }); + + printf("listening on: %s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port); + svr.listen(svr_params.listen_ip, svr_params.listen_port); + + // cleanup + free_sd_ctx(sd_ctx); + return 0; +} diff --git a/format-code.sh b/format-code.sh index adad801f5..d2a75bdcc 100644 --- a/format-code.sh +++ b/format-code.sh @@ -1,4 +1,4 @@ -for f in *.cpp *.h *.hpp examples/cli/*.cpp examples/cli/*.h; do +for f in *.cpp *.h *.hpp examples/cli/*.cpp examples/common/*.hpp examples/cli/*.h examples/server/*.cpp; do [[ "$f" == vocab* ]] && continue echo "formatting '$f'" # if [ "$f" != "stable-diffusion.h" ]; then diff --git a/thirdparty/README.md b/thirdparty/README.md index 518dc18b1..ea44fa4a7 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -1,3 +1,10 @@ - json.hpp library from: https://github.com/nlohmann/json + - LICENSE: https://github.com/nlohmann/json/blob/develop/LICENSE.MIT - ZIP Library from: https://github.com/kuba--/zip -- darts.h from: https://github.com/google/sentencepiece/tree/master/third_party/darts_clone \ No newline at end of file + LICENSE: https://github.com/kuba--/zip/blob/master/LICENSE.txt +- darts.h from: https://github.com/google/sentencepiece/tree/master/third_party/darts_clone + - LICENSE: https://github.com/google/sentencepiece/blob/master/third_party/darts_clone/LICENSE +- httplib.h from: https://github.com/yhirose/cpp-httplib/blob/master/httplib.h + - LICENSE: https://github.com/yhirose/cpp-httplib/blob/master/LICENSE +- stb_image.h/stb_image_resize.h/stb_image_write.h from: https://github.com/nothings/stb + - LICENSE: https://github.com/nothings/stb/blob/master/LICENSE \ No newline at end of file diff --git a/thirdparty/httplib.h b/thirdparty/httplib.h new file mode 100644 index 000000000..62be061cd --- /dev/null +++ b/thirdparty/httplib.h @@ -0,0 +1,13303 @@ +// +// httplib.h +// +// Copyright (c) 2025 Yuji Hirose. All rights reserved. +// MIT License +// + +#ifndef CPPHTTPLIB_HTTPLIB_H +#define CPPHTTPLIB_HTTPLIB_H + +#define CPPHTTPLIB_VERSION "0.28.0" +#define CPPHTTPLIB_VERSION_NUM "0x001C00" + +/* + * Platform compatibility check + */ + +#if defined(_WIN32) && !defined(_WIN64) +#if defined(_MSC_VER) +#pragma message( \ + "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.") +#else +#warning \ + "cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler." +#endif +#elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8 +#warning \ + "cpp-httplib doesn't support 32-bit platforms. Please use a 64-bit compiler." +#elif defined(__SIZEOF_SIZE_T__) && __SIZEOF_SIZE_T__ < 8 +#warning \ + "cpp-httplib doesn't support platforms where size_t is less than 64 bits." +#endif + +#ifdef _WIN32 +#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 +#error \ + "cpp-httplib doesn't support Windows 8 or lower. Please use Windows 10 or later." +#endif +#endif + +/* + * Configuration + */ + +#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND 10000 +#endif + +#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT +#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 100 +#endif + +#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND +#define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300 +#endif + +#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND +#define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND +#define CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND +#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND +#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND 300 +#endif + +#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND +#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND +#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND +#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND +#define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0 +#endif + +#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND +#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0 +#endif + +#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND +#ifdef _WIN32 +#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000 +#else +#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0 +#endif +#endif + +#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH +#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192 +#endif + +#ifndef CPPHTTPLIB_HEADER_MAX_LENGTH +#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192 +#endif + +#ifndef CPPHTTPLIB_HEADER_MAX_COUNT +#define CPPHTTPLIB_HEADER_MAX_COUNT 100 +#endif + +#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT +#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 +#endif + +#ifndef CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT +#define CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT 1024 +#endif + +#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH +#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH ((std::numeric_limits::max)()) +#endif + +#ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH +#define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 +#endif + +#ifndef CPPHTTPLIB_RANGE_MAX_COUNT +#define CPPHTTPLIB_RANGE_MAX_COUNT 1024 +#endif + +#ifndef CPPHTTPLIB_TCP_NODELAY +#define CPPHTTPLIB_TCP_NODELAY false +#endif + +#ifndef CPPHTTPLIB_IPV6_V6ONLY +#define CPPHTTPLIB_IPV6_V6ONLY false +#endif + +#ifndef CPPHTTPLIB_RECV_BUFSIZ +#define CPPHTTPLIB_RECV_BUFSIZ size_t(16384u) +#endif + +#ifndef CPPHTTPLIB_SEND_BUFSIZ +#define CPPHTTPLIB_SEND_BUFSIZ size_t(16384u) +#endif + +#ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ +#define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u) +#endif + +#ifndef CPPHTTPLIB_THREAD_POOL_COUNT +#define CPPHTTPLIB_THREAD_POOL_COUNT \ + ((std::max)(8u, std::thread::hardware_concurrency() > 0 \ + ? std::thread::hardware_concurrency() - 1 \ + : 0)) +#endif + +#ifndef CPPHTTPLIB_RECV_FLAGS +#define CPPHTTPLIB_RECV_FLAGS 0 +#endif + +#ifndef CPPHTTPLIB_SEND_FLAGS +#define CPPHTTPLIB_SEND_FLAGS 0 +#endif + +#ifndef CPPHTTPLIB_LISTEN_BACKLOG +#define CPPHTTPLIB_LISTEN_BACKLOG 5 +#endif + +#ifndef CPPHTTPLIB_MAX_LINE_LENGTH +#define CPPHTTPLIB_MAX_LINE_LENGTH 32768 +#endif + +/* + * Headers + */ + +#ifdef _WIN32 +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif //_CRT_SECURE_NO_WARNINGS + +#ifndef _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_NONSTDC_NO_DEPRECATE +#endif //_CRT_NONSTDC_NO_DEPRECATE + +#if defined(_MSC_VER) +#if _MSC_VER < 1900 +#error Sorry, Visual Studio versions prior to 2015 are not supported +#endif + +#pragma comment(lib, "ws2_32.lib") + +using ssize_t = __int64; +#endif // _MSC_VER + +#ifndef S_ISREG +#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG) +#endif // S_ISREG + +#ifndef S_ISDIR +#define S_ISDIR(m) (((m) & S_IFDIR) == S_IFDIR) +#endif // S_ISDIR + +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX + +#include +#include +#include + +#if defined(__has_include) +#if __has_include() +// afunix.h uses types declared in winsock2.h, so has to be included after it. +#include +#define CPPHTTPLIB_HAVE_AFUNIX_H 1 +#endif +#endif + +#ifndef WSA_FLAG_NO_HANDLE_INHERIT +#define WSA_FLAG_NO_HANDLE_INHERIT 0x80 +#endif + +using nfds_t = unsigned long; +using socket_t = SOCKET; +using socklen_t = int; + +#else // not _WIN32 + +#include +#if !defined(_AIX) && !defined(__MVS__) +#include +#endif +#ifdef __MVS__ +#include +#ifndef NI_MAXHOST +#define NI_MAXHOST 1025 +#endif +#endif +#include +#include +#include +#ifdef __linux__ +#include +#undef _res // Undefine _res macro to avoid conflicts with user code (#2278) +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +using socket_t = int; +#ifndef INVALID_SOCKET +#define INVALID_SOCKET (-1) +#endif +#endif //_WIN32 + +#if defined(__APPLE__) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) || \ + defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) +#if TARGET_OS_MAC +#include +#include +#endif +#endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO or + // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef _WIN32 +#include + +// these are defined in wincrypt.h and it breaks compilation if BoringSSL is +// used +#undef X509_NAME +#undef X509_CERT_PAIR +#undef X509_EXTENSIONS +#undef PKCS7_SIGNER_INFO + +#ifdef _MSC_VER +#pragma comment(lib, "crypt32.lib") +#endif +#endif // _WIN32 + +#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) +#if TARGET_OS_MAC +#include +#endif +#endif // CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO + +#include +#include +#include +#include + +#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) +#include +#endif + +#include +#include + +#if defined(OPENSSL_IS_BORINGSSL) || defined(LIBRESSL_VERSION_NUMBER) +#if OPENSSL_VERSION_NUMBER < 0x1010107f +#error Please use OpenSSL or a current version of BoringSSL +#endif +#define SSL_get1_peer_certificate SSL_get_peer_certificate +#elif OPENSSL_VERSION_NUMBER < 0x30000000L +#error Sorry, OpenSSL versions prior to 3.0.0 are not supported +#endif + +#endif // CPPHTTPLIB_OPENSSL_SUPPORT + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +#include +#include +#endif + +#ifdef CPPHTTPLIB_ZSTD_SUPPORT +#include +#endif + +/* + * Declaration + */ +namespace httplib { + +namespace detail { + +/* + * Backport std::make_unique from C++14. + * + * NOTE: This code came up with the following stackoverflow post: + * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique + * + */ + +template +typename std::enable_if::value, std::unique_ptr>::type +make_unique(Args &&...args) { + return std::unique_ptr(new T(std::forward(args)...)); +} + +template +typename std::enable_if::value, std::unique_ptr>::type +make_unique(std::size_t n) { + typedef typename std::remove_extent::type RT; + return std::unique_ptr(new RT[n]); +} + +namespace case_ignore { + +inline unsigned char to_lower(int c) { + const static unsigned char table[256] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, + 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, + 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 224, 225, 226, + 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, + 242, 243, 244, 245, 246, 215, 248, 249, 250, 251, 252, 253, 254, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, + 255, + }; + return table[(unsigned char)(char)c]; +} + +inline bool equal(const std::string &a, const std::string &b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) { + return to_lower(ca) == to_lower(cb); + }); +} + +struct equal_to { + bool operator()(const std::string &a, const std::string &b) const { + return equal(a, b); + } +}; + +struct hash { + size_t operator()(const std::string &key) const { + return hash_core(key.data(), key.size(), 0); + } + + size_t hash_core(const char *s, size_t l, size_t h) const { + return (l == 0) ? h + : hash_core(s + 1, l - 1, + // Unsets the 6 high bits of h, therefore no + // overflow happens + (((std::numeric_limits::max)() >> 6) & + h * 33) ^ + static_cast(to_lower(*s))); + } +}; + +template +using unordered_set = std::unordered_set; + +} // namespace case_ignore + +// This is based on +// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". + +struct scope_exit { + explicit scope_exit(std::function &&f) + : exit_function(std::move(f)), execute_on_destruction{true} {} + + scope_exit(scope_exit &&rhs) noexcept + : exit_function(std::move(rhs.exit_function)), + execute_on_destruction{rhs.execute_on_destruction} { + rhs.release(); + } + + ~scope_exit() { + if (execute_on_destruction) { this->exit_function(); } + } + + void release() { this->execute_on_destruction = false; } + +private: + scope_exit(const scope_exit &) = delete; + void operator=(const scope_exit &) = delete; + scope_exit &operator=(scope_exit &&) = delete; + + std::function exit_function; + bool execute_on_destruction; +}; + +} // namespace detail + +enum SSLVerifierResponse { + // no decision has been made, use the built-in certificate verifier + NoDecisionMade, + // connection certificate is verified and accepted + CertificateAccepted, + // connection certificate was processed but is rejected + CertificateRejected +}; + +enum StatusCode { + // Information responses + Continue_100 = 100, + SwitchingProtocol_101 = 101, + Processing_102 = 102, + EarlyHints_103 = 103, + + // Successful responses + OK_200 = 200, + Created_201 = 201, + Accepted_202 = 202, + NonAuthoritativeInformation_203 = 203, + NoContent_204 = 204, + ResetContent_205 = 205, + PartialContent_206 = 206, + MultiStatus_207 = 207, + AlreadyReported_208 = 208, + IMUsed_226 = 226, + + // Redirection messages + MultipleChoices_300 = 300, + MovedPermanently_301 = 301, + Found_302 = 302, + SeeOther_303 = 303, + NotModified_304 = 304, + UseProxy_305 = 305, + unused_306 = 306, + TemporaryRedirect_307 = 307, + PermanentRedirect_308 = 308, + + // Client error responses + BadRequest_400 = 400, + Unauthorized_401 = 401, + PaymentRequired_402 = 402, + Forbidden_403 = 403, + NotFound_404 = 404, + MethodNotAllowed_405 = 405, + NotAcceptable_406 = 406, + ProxyAuthenticationRequired_407 = 407, + RequestTimeout_408 = 408, + Conflict_409 = 409, + Gone_410 = 410, + LengthRequired_411 = 411, + PreconditionFailed_412 = 412, + PayloadTooLarge_413 = 413, + UriTooLong_414 = 414, + UnsupportedMediaType_415 = 415, + RangeNotSatisfiable_416 = 416, + ExpectationFailed_417 = 417, + ImATeapot_418 = 418, + MisdirectedRequest_421 = 421, + UnprocessableContent_422 = 422, + Locked_423 = 423, + FailedDependency_424 = 424, + TooEarly_425 = 425, + UpgradeRequired_426 = 426, + PreconditionRequired_428 = 428, + TooManyRequests_429 = 429, + RequestHeaderFieldsTooLarge_431 = 431, + UnavailableForLegalReasons_451 = 451, + + // Server error responses + InternalServerError_500 = 500, + NotImplemented_501 = 501, + BadGateway_502 = 502, + ServiceUnavailable_503 = 503, + GatewayTimeout_504 = 504, + HttpVersionNotSupported_505 = 505, + VariantAlsoNegotiates_506 = 506, + InsufficientStorage_507 = 507, + LoopDetected_508 = 508, + NotExtended_510 = 510, + NetworkAuthenticationRequired_511 = 511, +}; + +using Headers = + std::unordered_multimap; + +using Params = std::multimap; +using Match = std::smatch; + +using DownloadProgress = std::function; +using UploadProgress = std::function; + +struct Response; +using ResponseHandler = std::function; + +struct FormData { + std::string name; + std::string content; + std::string filename; + std::string content_type; + Headers headers; +}; + +struct FormField { + std::string name; + std::string content; + Headers headers; +}; +using FormFields = std::multimap; + +using FormFiles = std::multimap; + +struct MultipartFormData { + FormFields fields; // Text fields from multipart + FormFiles files; // Files from multipart + + // Text field access + std::string get_field(const std::string &key, size_t id = 0) const; + std::vector get_fields(const std::string &key) const; + bool has_field(const std::string &key) const; + size_t get_field_count(const std::string &key) const; + + // File access + FormData get_file(const std::string &key, size_t id = 0) const; + std::vector get_files(const std::string &key) const; + bool has_file(const std::string &key) const; + size_t get_file_count(const std::string &key) const; +}; + +struct UploadFormData { + std::string name; + std::string content; + std::string filename; + std::string content_type; +}; +using UploadFormDataItems = std::vector; + +class DataSink { +public: + DataSink() : os(&sb_), sb_(*this) {} + + DataSink(const DataSink &) = delete; + DataSink &operator=(const DataSink &) = delete; + DataSink(DataSink &&) = delete; + DataSink &operator=(DataSink &&) = delete; + + std::function write; + std::function is_writable; + std::function done; + std::function done_with_trailer; + std::ostream os; + +private: + class data_sink_streambuf final : public std::streambuf { + public: + explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {} + + protected: + std::streamsize xsputn(const char *s, std::streamsize n) override { + sink_.write(s, static_cast(n)); + return n; + } + + private: + DataSink &sink_; + }; + + data_sink_streambuf sb_; +}; + +using ContentProvider = + std::function; + +using ContentProviderWithoutLength = + std::function; + +using ContentProviderResourceReleaser = std::function; + +struct FormDataProvider { + std::string name; + ContentProviderWithoutLength provider; + std::string filename; + std::string content_type; +}; +using FormDataProviderItems = std::vector; + +using ContentReceiverWithProgress = std::function; + +using ContentReceiver = + std::function; + +using FormDataHeader = std::function; + +class ContentReader { +public: + using Reader = std::function; + using FormDataReader = + std::function; + + ContentReader(Reader reader, FormDataReader multipart_reader) + : reader_(std::move(reader)), + formdata_reader_(std::move(multipart_reader)) {} + + bool operator()(FormDataHeader header, ContentReceiver receiver) const { + return formdata_reader_(std::move(header), std::move(receiver)); + } + + bool operator()(ContentReceiver receiver) const { + return reader_(std::move(receiver)); + } + + Reader reader_; + FormDataReader formdata_reader_; +}; + +using Range = std::pair; +using Ranges = std::vector; + +struct Request { + std::string method; + std::string path; + std::string matched_route; + Params params; + Headers headers; + Headers trailers; + std::string body; + + std::string remote_addr; + int remote_port = -1; + std::string local_addr; + int local_port = -1; + + // for server + std::string version; + std::string target; + MultipartFormData form; + Ranges ranges; + Match matches; + std::unordered_map path_params; + std::function is_connection_closed = []() { return true; }; + + // for client + std::vector accept_content_types; + ResponseHandler response_handler; + ContentReceiverWithProgress content_receiver; + DownloadProgress download_progress; + UploadProgress upload_progress; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + const SSL *ssl = nullptr; +#endif + + bool has_header(const std::string &key) const; + std::string get_header_value(const std::string &key, const char *def = "", + size_t id = 0) const; + size_t get_header_value_u64(const std::string &key, size_t def = 0, + size_t id = 0) const; + size_t get_header_value_count(const std::string &key) const; + void set_header(const std::string &key, const std::string &val); + + bool has_trailer(const std::string &key) const; + std::string get_trailer_value(const std::string &key, size_t id = 0) const; + size_t get_trailer_value_count(const std::string &key) const; + + bool has_param(const std::string &key) const; + std::string get_param_value(const std::string &key, size_t id = 0) const; + size_t get_param_value_count(const std::string &key) const; + + bool is_multipart_form_data() const; + + // private members... + size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT; + size_t content_length_ = 0; + ContentProvider content_provider_; + bool is_chunked_content_provider_ = false; + size_t authorization_count_ = 0; + std::chrono::time_point start_time_ = + (std::chrono::steady_clock::time_point::min)(); +}; + +struct Response { + std::string version; + int status = -1; + std::string reason; + Headers headers; + Headers trailers; + std::string body; + std::string location; // Redirect location + + bool has_header(const std::string &key) const; + std::string get_header_value(const std::string &key, const char *def = "", + size_t id = 0) const; + size_t get_header_value_u64(const std::string &key, size_t def = 0, + size_t id = 0) const; + size_t get_header_value_count(const std::string &key) const; + void set_header(const std::string &key, const std::string &val); + + bool has_trailer(const std::string &key) const; + std::string get_trailer_value(const std::string &key, size_t id = 0) const; + size_t get_trailer_value_count(const std::string &key) const; + + void set_redirect(const std::string &url, int status = StatusCode::Found_302); + void set_content(const char *s, size_t n, const std::string &content_type); + void set_content(const std::string &s, const std::string &content_type); + void set_content(std::string &&s, const std::string &content_type); + + void set_content_provider( + size_t length, const std::string &content_type, ContentProvider provider, + ContentProviderResourceReleaser resource_releaser = nullptr); + + void set_content_provider( + const std::string &content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser = nullptr); + + void set_chunked_content_provider( + const std::string &content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser = nullptr); + + void set_file_content(const std::string &path, + const std::string &content_type); + void set_file_content(const std::string &path); + + Response() = default; + Response(const Response &) = default; + Response &operator=(const Response &) = default; + Response(Response &&) = default; + Response &operator=(Response &&) = default; + ~Response() { + if (content_provider_resource_releaser_) { + content_provider_resource_releaser_(content_provider_success_); + } + } + + // private members... + size_t content_length_ = 0; + ContentProvider content_provider_; + ContentProviderResourceReleaser content_provider_resource_releaser_; + bool is_chunked_content_provider_ = false; + bool content_provider_success_ = false; + std::string file_content_path_; + std::string file_content_content_type_; +}; + +class Stream { +public: + virtual ~Stream() = default; + + virtual bool is_readable() const = 0; + virtual bool wait_readable() const = 0; + virtual bool wait_writable() const = 0; + + virtual ssize_t read(char *ptr, size_t size) = 0; + virtual ssize_t write(const char *ptr, size_t size) = 0; + virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0; + virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0; + virtual socket_t socket() const = 0; + + virtual time_t duration() const = 0; + + ssize_t write(const char *ptr); + ssize_t write(const std::string &s); +}; + +class TaskQueue { +public: + TaskQueue() = default; + virtual ~TaskQueue() = default; + + virtual bool enqueue(std::function fn) = 0; + virtual void shutdown() = 0; + + virtual void on_idle() {} +}; + +class ThreadPool final : public TaskQueue { +public: + explicit ThreadPool(size_t n, size_t mqr = 0) + : shutdown_(false), max_queued_requests_(mqr) { + threads_.reserve(n); + while (n) { + threads_.emplace_back(worker(*this)); + n--; + } + } + + ThreadPool(const ThreadPool &) = delete; + ~ThreadPool() override = default; + + bool enqueue(std::function fn) override { + { + std::unique_lock lock(mutex_); + if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) { + return false; + } + jobs_.push_back(std::move(fn)); + } + + cond_.notify_one(); + return true; + } + + void shutdown() override { + // Stop all worker threads... + { + std::unique_lock lock(mutex_); + shutdown_ = true; + } + + cond_.notify_all(); + + // Join... + for (auto &t : threads_) { + t.join(); + } + } + +private: + struct worker { + explicit worker(ThreadPool &pool) : pool_(pool) {} + + void operator()() { + for (;;) { + std::function fn; + { + std::unique_lock lock(pool_.mutex_); + + pool_.cond_.wait( + lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; }); + + if (pool_.shutdown_ && pool_.jobs_.empty()) { break; } + + fn = pool_.jobs_.front(); + pool_.jobs_.pop_front(); + } + + assert(true == static_cast(fn)); + fn(); + } + +#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) && \ + !defined(LIBRESSL_VERSION_NUMBER) + OPENSSL_thread_stop(); +#endif + } + + ThreadPool &pool_; + }; + friend struct worker; + + std::vector threads_; + std::list> jobs_; + + bool shutdown_; + size_t max_queued_requests_ = 0; + + std::condition_variable cond_; + std::mutex mutex_; +}; + +using Logger = std::function; + +// Forward declaration for Error type +enum class Error; +using ErrorLogger = std::function; + +using SocketOptions = std::function; + +namespace detail { + +bool set_socket_opt_impl(socket_t sock, int level, int optname, + const void *optval, socklen_t optlen); +bool set_socket_opt(socket_t sock, int level, int optname, int opt); +bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, + time_t usec); +int close_socket(socket_t sock); + +} // namespace detail + +void default_socket_options(socket_t sock); + +const char *status_message(int status); + +std::string get_bearer_token_auth(const Request &req); + +namespace detail { + +class MatcherBase { +public: + MatcherBase(std::string pattern) : pattern_(std::move(pattern)) {} + virtual ~MatcherBase() = default; + + const std::string &pattern() const { return pattern_; } + + // Match request path and populate its matches and + virtual bool match(Request &request) const = 0; + +private: + std::string pattern_; +}; + +/** + * Captures parameters in request path and stores them in Request::path_params + * + * Capture name is a substring of a pattern from : to /. + * The rest of the pattern is matched against the request path directly + * Parameters are captured starting from the next character after + * the end of the last matched static pattern fragment until the next /. + * + * Example pattern: + * "/path/fragments/:capture/more/fragments/:second_capture" + * Static fragments: + * "/path/fragments/", "more/fragments/" + * + * Given the following request path: + * "/path/fragments/:1/more/fragments/:2" + * the resulting capture will be + * {{"capture", "1"}, {"second_capture", "2"}} + */ +class PathParamsMatcher final : public MatcherBase { +public: + PathParamsMatcher(const std::string &pattern); + + bool match(Request &request) const override; + +private: + // Treat segment separators as the end of path parameter capture + // Does not need to handle query parameters as they are parsed before path + // matching + static constexpr char separator = '/'; + + // Contains static path fragments to match against, excluding the '/' after + // path params + // Fragments are separated by path params + std::vector static_fragments_; + // Stores the names of the path parameters to be used as keys in the + // Request::path_params map + std::vector param_names_; +}; + +/** + * Performs std::regex_match on request path + * and stores the result in Request::matches + * + * Note that regex match is performed directly on the whole request. + * This means that wildcard patterns may match multiple path segments with /: + * "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end". + */ +class RegexMatcher final : public MatcherBase { +public: + RegexMatcher(const std::string &pattern) + : MatcherBase(pattern), regex_(pattern) {} + + bool match(Request &request) const override; + +private: + std::regex regex_; +}; + +ssize_t write_headers(Stream &strm, const Headers &headers); + +std::string make_host_and_port_string(const std::string &host, int port, + bool is_ssl); + +} // namespace detail + +class Server { +public: + using Handler = std::function; + + using ExceptionHandler = + std::function; + + enum class HandlerResponse { + Handled, + Unhandled, + }; + using HandlerWithResponse = + std::function; + + using HandlerWithContentReader = std::function; + + using Expect100ContinueHandler = + std::function; + + Server(); + + virtual ~Server(); + + virtual bool is_valid() const; + + Server &Get(const std::string &pattern, Handler handler); + Server &Post(const std::string &pattern, Handler handler); + Server &Post(const std::string &pattern, HandlerWithContentReader handler); + Server &Put(const std::string &pattern, Handler handler); + Server &Put(const std::string &pattern, HandlerWithContentReader handler); + Server &Patch(const std::string &pattern, Handler handler); + Server &Patch(const std::string &pattern, HandlerWithContentReader handler); + Server &Delete(const std::string &pattern, Handler handler); + Server &Delete(const std::string &pattern, HandlerWithContentReader handler); + Server &Options(const std::string &pattern, Handler handler); + + bool set_base_dir(const std::string &dir, + const std::string &mount_point = std::string()); + bool set_mount_point(const std::string &mount_point, const std::string &dir, + Headers headers = Headers()); + bool remove_mount_point(const std::string &mount_point); + Server &set_file_extension_and_mimetype_mapping(const std::string &ext, + const std::string &mime); + Server &set_default_file_mimetype(const std::string &mime); + Server &set_file_request_handler(Handler handler); + + template + Server &set_error_handler(ErrorHandlerFunc &&handler) { + return set_error_handler_core( + std::forward(handler), + std::is_convertible{}); + } + + Server &set_exception_handler(ExceptionHandler handler); + + Server &set_pre_routing_handler(HandlerWithResponse handler); + Server &set_post_routing_handler(Handler handler); + + Server &set_pre_request_handler(HandlerWithResponse handler); + + Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); + Server &set_logger(Logger logger); + Server &set_pre_compression_logger(Logger logger); + Server &set_error_logger(ErrorLogger error_logger); + + Server &set_address_family(int family); + Server &set_tcp_nodelay(bool on); + Server &set_ipv6_v6only(bool on); + Server &set_socket_options(SocketOptions socket_options); + + Server &set_default_headers(Headers headers); + Server & + set_header_writer(std::function const &writer); + + Server &set_trusted_proxies(const std::vector &proxies); + + Server &set_keep_alive_max_count(size_t count); + Server &set_keep_alive_timeout(time_t sec); + + Server &set_read_timeout(time_t sec, time_t usec = 0); + template + Server &set_read_timeout(const std::chrono::duration &duration); + + Server &set_write_timeout(time_t sec, time_t usec = 0); + template + Server &set_write_timeout(const std::chrono::duration &duration); + + Server &set_idle_interval(time_t sec, time_t usec = 0); + template + Server &set_idle_interval(const std::chrono::duration &duration); + + Server &set_payload_max_length(size_t length); + + bool bind_to_port(const std::string &host, int port, int socket_flags = 0); + int bind_to_any_port(const std::string &host, int socket_flags = 0); + bool listen_after_bind(); + + bool listen(const std::string &host, int port, int socket_flags = 0); + + bool is_running() const; + void wait_until_ready() const; + void stop(); + void decommission(); + + std::function new_task_queue; + +protected: + bool process_request(Stream &strm, const std::string &remote_addr, + int remote_port, const std::string &local_addr, + int local_port, bool close_connection, + bool &connection_closed, + const std::function &setup_request); + + std::atomic svr_sock_{INVALID_SOCKET}; + + std::vector trusted_proxies_; + + size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; + time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND; + time_t read_timeout_sec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND; + time_t read_timeout_usec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND; + time_t write_timeout_sec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND; + time_t write_timeout_usec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND; + time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND; + time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND; + size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; + +private: + using Handlers = + std::vector, Handler>>; + using HandlersForContentReader = + std::vector, + HandlerWithContentReader>>; + + static std::unique_ptr + make_matcher(const std::string &pattern); + + Server &set_error_handler_core(HandlerWithResponse handler, std::true_type); + Server &set_error_handler_core(Handler handler, std::false_type); + + socket_t create_server_socket(const std::string &host, int port, + int socket_flags, + SocketOptions socket_options) const; + int bind_internal(const std::string &host, int port, int socket_flags); + bool listen_internal(); + + bool routing(Request &req, Response &res, Stream &strm); + bool handle_file_request(const Request &req, Response &res); + bool dispatch_request(Request &req, Response &res, + const Handlers &handlers) const; + bool dispatch_request_for_content_reader( + Request &req, Response &res, ContentReader content_reader, + const HandlersForContentReader &handlers) const; + + bool parse_request_line(const char *s, Request &req) const; + void apply_ranges(const Request &req, Response &res, + std::string &content_type, std::string &boundary) const; + bool write_response(Stream &strm, bool close_connection, Request &req, + Response &res); + bool write_response_with_content(Stream &strm, bool close_connection, + const Request &req, Response &res); + bool write_response_core(Stream &strm, bool close_connection, + const Request &req, Response &res, + bool need_apply_ranges); + bool write_content_with_provider(Stream &strm, const Request &req, + Response &res, const std::string &boundary, + const std::string &content_type); + bool read_content(Stream &strm, Request &req, Response &res); + bool read_content_with_content_receiver(Stream &strm, Request &req, + Response &res, + ContentReceiver receiver, + FormDataHeader multipart_header, + ContentReceiver multipart_receiver); + bool read_content_core(Stream &strm, Request &req, Response &res, + ContentReceiver receiver, + FormDataHeader multipart_header, + ContentReceiver multipart_receiver) const; + + virtual bool process_and_close_socket(socket_t sock); + + void output_log(const Request &req, const Response &res) const; + void output_pre_compression_log(const Request &req, + const Response &res) const; + void output_error_log(const Error &err, const Request *req) const; + + std::atomic is_running_{false}; + std::atomic is_decommissioned{false}; + + struct MountPointEntry { + std::string mount_point; + std::string base_dir; + Headers headers; + }; + std::vector base_dirs_; + std::map file_extension_and_mimetype_map_; + std::string default_file_mimetype_ = "application/octet-stream"; + Handler file_request_handler_; + + Handlers get_handlers_; + Handlers post_handlers_; + HandlersForContentReader post_handlers_for_content_reader_; + Handlers put_handlers_; + HandlersForContentReader put_handlers_for_content_reader_; + Handlers patch_handlers_; + HandlersForContentReader patch_handlers_for_content_reader_; + Handlers delete_handlers_; + HandlersForContentReader delete_handlers_for_content_reader_; + Handlers options_handlers_; + + HandlerWithResponse error_handler_; + ExceptionHandler exception_handler_; + HandlerWithResponse pre_routing_handler_; + Handler post_routing_handler_; + HandlerWithResponse pre_request_handler_; + Expect100ContinueHandler expect_100_continue_handler_; + + mutable std::mutex logger_mutex_; + Logger logger_; + Logger pre_compression_logger_; + ErrorLogger error_logger_; + + int address_family_ = AF_UNSPEC; + bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; + bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; + SocketOptions socket_options_ = default_socket_options; + + Headers default_headers_; + std::function header_writer_ = + detail::write_headers; +}; + +enum class Error { + Success = 0, + Unknown, + Connection, + BindIPAddress, + Read, + Write, + ExceedRedirectCount, + Canceled, + SSLConnection, + SSLLoadingCerts, + SSLServerVerification, + SSLServerHostnameVerification, + UnsupportedMultipartBoundaryChars, + Compression, + ConnectionTimeout, + ProxyConnection, + ResourceExhaustion, + TooManyFormDataFiles, + ExceedMaxPayloadSize, + ExceedUriMaxLength, + ExceedMaxSocketDescriptorCount, + InvalidRequestLine, + InvalidHTTPMethod, + InvalidHTTPVersion, + InvalidHeaders, + MultipartParsing, + OpenFile, + Listen, + GetSockName, + UnsupportedAddressFamily, + HTTPParsing, + InvalidRangeHeader, + + // For internal use only + SSLPeerCouldBeClosed_, +}; + +std::string to_string(Error error); + +std::ostream &operator<<(std::ostream &os, const Error &obj); + +class Result { +public: + Result() = default; + Result(std::unique_ptr &&res, Error err, + Headers &&request_headers = Headers{}) + : res_(std::move(res)), err_(err), + request_headers_(std::move(request_headers)) {} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + Result(std::unique_ptr &&res, Error err, Headers &&request_headers, + int ssl_error) + : res_(std::move(res)), err_(err), + request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {} + Result(std::unique_ptr &&res, Error err, Headers &&request_headers, + int ssl_error, unsigned long ssl_openssl_error) + : res_(std::move(res)), err_(err), + request_headers_(std::move(request_headers)), ssl_error_(ssl_error), + ssl_openssl_error_(ssl_openssl_error) {} +#endif + // Response + operator bool() const { return res_ != nullptr; } + bool operator==(std::nullptr_t) const { return res_ == nullptr; } + bool operator!=(std::nullptr_t) const { return res_ != nullptr; } + const Response &value() const { return *res_; } + Response &value() { return *res_; } + const Response &operator*() const { return *res_; } + Response &operator*() { return *res_; } + const Response *operator->() const { return res_.get(); } + Response *operator->() { return res_.get(); } + + // Error + Error error() const { return err_; } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // SSL Error + int ssl_error() const { return ssl_error_; } + // OpenSSL Error + unsigned long ssl_openssl_error() const { return ssl_openssl_error_; } +#endif + + // Request Headers + bool has_request_header(const std::string &key) const; + std::string get_request_header_value(const std::string &key, + const char *def = "", + size_t id = 0) const; + size_t get_request_header_value_u64(const std::string &key, size_t def = 0, + size_t id = 0) const; + size_t get_request_header_value_count(const std::string &key) const; + +private: + std::unique_ptr res_; + Error err_ = Error::Unknown; + Headers request_headers_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + int ssl_error_ = 0; + unsigned long ssl_openssl_error_ = 0; +#endif +}; + +struct ClientConnection { + socket_t sock = INVALID_SOCKET; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSL *ssl = nullptr; +#endif + + bool is_open() const { return sock != INVALID_SOCKET; } + + ClientConnection() = default; + + ~ClientConnection() { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (ssl) { + SSL_free(ssl); + ssl = nullptr; + } +#endif + if (sock != INVALID_SOCKET) { + detail::close_socket(sock); + sock = INVALID_SOCKET; + } + } + + ClientConnection(const ClientConnection &) = delete; + ClientConnection &operator=(const ClientConnection &) = delete; + + ClientConnection(ClientConnection &&other) noexcept + : sock(other.sock) +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + , + ssl(other.ssl) +#endif + { + other.sock = INVALID_SOCKET; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + other.ssl = nullptr; +#endif + } + + ClientConnection &operator=(ClientConnection &&other) noexcept { + if (this != &other) { + sock = other.sock; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + ssl = other.ssl; +#endif + other.sock = INVALID_SOCKET; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + other.ssl = nullptr; +#endif + } + return *this; + } +}; + +namespace detail { + +struct ChunkedDecoder; + +struct BodyReader { + Stream *stream = nullptr; + size_t content_length = 0; + size_t bytes_read = 0; + bool chunked = false; + bool eof = false; + std::unique_ptr chunked_decoder; + Error last_error = Error::Success; + + ssize_t read(char *buf, size_t len); + bool has_error() const { return last_error != Error::Success; } +}; + +inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf, + size_t len) { + (void)stream; + return br.read(buf, len); +} + +class decompressor; + +} // namespace detail + +class ClientImpl { +public: + explicit ClientImpl(const std::string &host); + + explicit ClientImpl(const std::string &host, int port); + + explicit ClientImpl(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path); + + virtual ~ClientImpl(); + + virtual bool is_valid() const; + + struct StreamHandle { + std::unique_ptr response; + Error error = Error::Success; + + StreamHandle() = default; + StreamHandle(const StreamHandle &) = delete; + StreamHandle &operator=(const StreamHandle &) = delete; + StreamHandle(StreamHandle &&) = default; + StreamHandle &operator=(StreamHandle &&) = default; + ~StreamHandle() = default; + + bool is_valid() const { + return response != nullptr && error == Error::Success; + } + + ssize_t read(char *buf, size_t len); + void parse_trailers_if_needed(); + Error get_read_error() const { return body_reader_.last_error; } + bool has_read_error() const { return body_reader_.has_error(); } + + bool trailers_parsed_ = false; + + private: + friend class ClientImpl; + + ssize_t read_with_decompression(char *buf, size_t len); + + std::unique_ptr connection_; + std::unique_ptr socket_stream_; + Stream *stream_ = nullptr; + detail::BodyReader body_reader_; + + std::unique_ptr decompressor_; + std::string decompress_buffer_; + size_t decompress_offset_ = 0; + }; + + // clang-format off + Result Get(const std::string &path, DownloadProgress progress = nullptr); + Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Head(const std::string &path); + Result Head(const std::string &path, const Headers &headers); + + Result Post(const std::string &path); + Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Params ¶ms); + Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers); + Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const Params ¶ms); + Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Put(const std::string &path); + Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Params ¶ms); + Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers); + Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const Params ¶ms); + Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Patch(const std::string &path); + Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Params ¶ms); + Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const Params ¶ms); + Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Delete(const std::string &path, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Params ¶ms, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress = nullptr); + + Result Options(const std::string &path); + Result Options(const std::string &path, const Headers &headers); + // clang-format on + + // Streaming API: Open a stream for reading response body incrementally + // Socket ownership is transferred to StreamHandle for true streaming + // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.) + StreamHandle open_stream(const std::string &method, const std::string &path, + const Params ¶ms = {}, + const Headers &headers = {}, + const std::string &body = {}, + const std::string &content_type = {}); + + bool send(Request &req, Response &res, Error &error); + Result send(const Request &req); + + void stop(); + + std::string host() const; + int port() const; + + size_t is_socket_open() const; + socket_t socket() const; + + void set_hostname_addr_map(std::map addr_map); + + void set_default_headers(Headers headers); + + void + set_header_writer(std::function const &writer); + + void set_address_family(int family); + void set_tcp_nodelay(bool on); + void set_ipv6_v6only(bool on); + void set_socket_options(SocketOptions socket_options); + + void set_connection_timeout(time_t sec, time_t usec = 0); + template + void + set_connection_timeout(const std::chrono::duration &duration); + + void set_read_timeout(time_t sec, time_t usec = 0); + template + void set_read_timeout(const std::chrono::duration &duration); + + void set_write_timeout(time_t sec, time_t usec = 0); + template + void set_write_timeout(const std::chrono::duration &duration); + + void set_max_timeout(time_t msec); + template + void set_max_timeout(const std::chrono::duration &duration); + + void set_basic_auth(const std::string &username, const std::string &password); + void set_bearer_token_auth(const std::string &token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_digest_auth(const std::string &username, + const std::string &password); +#endif + + void set_keep_alive(bool on); + void set_follow_location(bool on); + + void set_path_encode(bool on); + + void set_compress(bool on); + + void set_decompress(bool on); + + void set_interface(const std::string &intf); + + void set_proxy(const std::string &host, int port); + void set_proxy_basic_auth(const std::string &username, + const std::string &password); + void set_proxy_bearer_token_auth(const std::string &token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_proxy_digest_auth(const std::string &username, + const std::string &password); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path = std::string()); + void set_ca_cert_store(X509_STORE *ca_cert_store); + X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size) const; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void enable_server_certificate_verification(bool enabled); + void enable_server_hostname_verification(bool enabled); + void set_server_certificate_verifier( + std::function verifier); +#endif + + void set_logger(Logger logger); + void set_error_logger(ErrorLogger error_logger); + +protected: + struct Socket { + socket_t sock = INVALID_SOCKET; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSL *ssl = nullptr; +#endif + + bool is_open() const { return sock != INVALID_SOCKET; } + }; + + virtual bool create_and_connect_socket(Socket &socket, Error &error); + virtual bool ensure_socket_connection(Socket &socket, Error &error); + + // All of: + // shutdown_ssl + // shutdown_socket + // close_socket + // should ONLY be called when socket_mutex_ is locked. + // Also, shutdown_ssl and close_socket should also NOT be called concurrently + // with a DIFFERENT thread sending requests using that socket. + virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully); + void shutdown_socket(Socket &socket) const; + void close_socket(Socket &socket); + + bool process_request(Stream &strm, Request &req, Response &res, + bool close_connection, Error &error); + + bool write_content_with_provider(Stream &strm, const Request &req, + Error &error) const; + + void copy_settings(const ClientImpl &rhs); + + void output_log(const Request &req, const Response &res) const; + void output_error_log(const Error &err, const Request *req) const; + + // Socket endpoint information + const std::string host_; + const int port_; + const std::string host_and_port_; + + // Current open socket + Socket socket_; + mutable std::mutex socket_mutex_; + std::recursive_mutex request_mutex_; + + // These are all protected under socket_mutex + size_t socket_requests_in_flight_ = 0; + std::thread::id socket_requests_are_from_thread_ = std::thread::id(); + bool socket_should_be_closed_when_request_is_done_ = false; + + // Hostname-IP map + std::map addr_map_; + + // Default headers + Headers default_headers_; + + // Header writer + std::function header_writer_ = + detail::write_headers; + + // Settings + std::string client_cert_path_; + std::string client_key_path_; + + time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND; + time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; + time_t read_timeout_sec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND; + time_t read_timeout_usec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND; + time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND; + time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND; + time_t max_timeout_msec_ = CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND; + + std::string basic_auth_username_; + std::string basic_auth_password_; + std::string bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string digest_auth_username_; + std::string digest_auth_password_; +#endif + + bool keep_alive_ = false; + bool follow_location_ = false; + + bool path_encode_ = true; + + int address_family_ = AF_UNSPEC; + bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; + bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; + SocketOptions socket_options_ = nullptr; + + bool compress_ = false; + bool decompress_ = true; + + std::string interface_; + + std::string proxy_host_; + int proxy_port_ = -1; + + std::string proxy_basic_auth_username_; + std::string proxy_basic_auth_password_; + std::string proxy_bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string proxy_digest_auth_username_; + std::string proxy_digest_auth_password_; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string ca_cert_file_path_; + std::string ca_cert_dir_path_; + + X509_STORE *ca_cert_store_ = nullptr; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + bool server_certificate_verification_ = true; + bool server_hostname_verification_ = true; + std::function server_certificate_verifier_; +#endif + + mutable std::mutex logger_mutex_; + Logger logger_; + ErrorLogger error_logger_; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + int last_ssl_error_ = 0; + unsigned long last_openssl_error_ = 0; +#endif + +private: + bool send_(Request &req, Response &res, Error &error); + Result send_(Request &&req); + + socket_t create_client_socket(Error &error) const; + bool read_response_line(Stream &strm, const Request &req, + Response &res) const; + bool write_request(Stream &strm, Request &req, bool close_connection, + Error &error); + void prepare_default_headers(Request &r, bool for_stream, + const std::string &ct); + bool redirect(Request &req, Response &res, Error &error); + bool create_redirect_client(const std::string &scheme, + const std::string &host, int port, Request &req, + Response &res, const std::string &path, + const std::string &location, Error &error); + template void setup_redirect_client(ClientType &client); + bool handle_request(Stream &strm, Request &req, Response &res, + bool close_connection, Error &error); + std::unique_ptr send_with_content_provider_and_receiver( + Request &req, const char *body, size_t content_length, + ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const std::string &content_type, ContentReceiver content_receiver, + Error &error); + Result send_with_content_provider_and_receiver( + const std::string &method, const std::string &path, + const Headers &headers, const char *body, size_t content_length, + ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const std::string &content_type, ContentReceiver content_receiver, + UploadProgress progress); + ContentProviderWithoutLength get_multipart_content_provider( + const std::string &boundary, const UploadFormDataItems &items, + const FormDataProviderItems &provider_items) const; + + virtual bool + process_socket(const Socket &socket, + std::chrono::time_point start_time, + std::function callback); + virtual bool is_ssl() const; + + void transfer_socket_ownership_to_handle(StreamHandle &handle); +}; + +class Client { +public: + // Universal interface + explicit Client(const std::string &scheme_host_port); + + explicit Client(const std::string &scheme_host_port, + const std::string &client_cert_path, + const std::string &client_key_path); + + // HTTP only interface + explicit Client(const std::string &host, int port); + + explicit Client(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path); + + Client(Client &&) = default; + Client &operator=(Client &&) = default; + + ~Client(); + + bool is_valid() const; + + // clang-format off + Result Get(const std::string &path, DownloadProgress progress = nullptr); + Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Head(const std::string &path); + Result Head(const std::string &path, const Headers &headers); + + Result Post(const std::string &path); + Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Params ¶ms); + Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers); + Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const Params ¶ms); + Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); + Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Put(const std::string &path); + Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Params ¶ms); + Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers); + Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const Params ¶ms); + Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); + Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Patch(const std::string &path); + Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Params ¶ms); + Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers); + Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const Params ¶ms); + Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); + Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); + + Result Delete(const std::string &path, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Params ¶ms, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); + Result Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress = nullptr); + + Result Options(const std::string &path); + Result Options(const std::string &path, const Headers &headers); + // clang-format on + + // Streaming API: Open a stream for reading response body incrementally + // Socket ownership is transferred to StreamHandle for true streaming + // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.) + ClientImpl::StreamHandle open_stream(const std::string &method, + const std::string &path, + const Params ¶ms = {}, + const Headers &headers = {}, + const std::string &body = {}, + const std::string &content_type = {}); + + bool send(Request &req, Response &res, Error &error); + Result send(const Request &req); + + void stop(); + + std::string host() const; + int port() const; + + size_t is_socket_open() const; + socket_t socket() const; + + void set_hostname_addr_map(std::map addr_map); + + void set_default_headers(Headers headers); + + void + set_header_writer(std::function const &writer); + + void set_address_family(int family); + void set_tcp_nodelay(bool on); + void set_socket_options(SocketOptions socket_options); + + void set_connection_timeout(time_t sec, time_t usec = 0); + template + void + set_connection_timeout(const std::chrono::duration &duration); + + void set_read_timeout(time_t sec, time_t usec = 0); + template + void set_read_timeout(const std::chrono::duration &duration); + + void set_write_timeout(time_t sec, time_t usec = 0); + template + void set_write_timeout(const std::chrono::duration &duration); + + void set_max_timeout(time_t msec); + template + void set_max_timeout(const std::chrono::duration &duration); + + void set_basic_auth(const std::string &username, const std::string &password); + void set_bearer_token_auth(const std::string &token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_digest_auth(const std::string &username, + const std::string &password); +#endif + + void set_keep_alive(bool on); + void set_follow_location(bool on); + + void set_path_encode(bool on); + void set_url_encode(bool on); + + void set_compress(bool on); + + void set_decompress(bool on); + + void set_interface(const std::string &intf); + + void set_proxy(const std::string &host, int port); + void set_proxy_basic_auth(const std::string &username, + const std::string &password); + void set_proxy_bearer_token_auth(const std::string &token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_proxy_digest_auth(const std::string &username, + const std::string &password); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void enable_server_certificate_verification(bool enabled); + void enable_server_hostname_verification(bool enabled); + void set_server_certificate_verifier( + std::function verifier); +#endif + + void set_logger(Logger logger); + void set_error_logger(ErrorLogger error_logger); + + // SSL +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path = std::string()); + + void set_ca_cert_store(X509_STORE *ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); + + long get_openssl_verify_result() const; + + SSL_CTX *ssl_context() const; +#endif + +private: + std::unique_ptr cli_; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + bool is_ssl_ = false; +#endif +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLServer : public Server { +public: + SSLServer(const char *cert_path, const char *private_key_path, + const char *client_ca_cert_file_path = nullptr, + const char *client_ca_cert_dir_path = nullptr, + const char *private_key_password = nullptr); + + SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store = nullptr); + + SSLServer( + const std::function &setup_ssl_ctx_callback); + + ~SSLServer() override; + + bool is_valid() const override; + + SSL_CTX *ssl_context() const; + + void update_certs(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store = nullptr); + + int ssl_last_error() const { return last_ssl_error_; } + +private: + bool process_and_close_socket(socket_t sock) override; + + STACK_OF(X509_NAME) * extract_ca_names_from_x509_store(X509_STORE *store); + + SSL_CTX *ctx_; + std::mutex ctx_mutex_; + + int last_ssl_error_ = 0; +}; + +class SSLClient final : public ClientImpl { +public: + explicit SSLClient(const std::string &host); + + explicit SSLClient(const std::string &host, int port); + + explicit SSLClient(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path, + const std::string &private_key_password = std::string()); + + explicit SSLClient(const std::string &host, int port, X509 *client_cert, + EVP_PKEY *client_key, + const std::string &private_key_password = std::string()); + + ~SSLClient() override; + + bool is_valid() const override; + + void set_ca_cert_store(X509_STORE *ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); + + long get_openssl_verify_result() const; + + SSL_CTX *ssl_context() const; + +private: + bool create_and_connect_socket(Socket &socket, Error &error) override; + bool ensure_socket_connection(Socket &socket, Error &error) override; + void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override; + void shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully); + + bool + process_socket(const Socket &socket, + std::chrono::time_point start_time, + std::function callback) override; + bool is_ssl() const override; + + bool connect_with_proxy( + Socket &sock, + std::chrono::time_point start_time, + Response &res, bool &success, Error &error); + bool initialize_ssl(Socket &socket, Error &error); + + bool load_certs(); + + bool verify_host(X509 *server_cert) const; + bool verify_host_with_subject_alt_name(X509 *server_cert) const; + bool verify_host_with_common_name(X509 *server_cert) const; + bool check_host_name(const char *pattern, size_t pattern_len) const; + + SSL_CTX *ctx_; + std::mutex ctx_mutex_; + std::once_flag initialize_cert_; + + std::vector host_components_; + + long verify_result_ = 0; + + friend class ClientImpl; +}; +#endif + +/* + * Implementation of template methods. + */ + +namespace detail { + +template +inline void duration_to_sec_and_usec(const T &duration, U callback) { + auto sec = std::chrono::duration_cast(duration).count(); + auto usec = std::chrono::duration_cast( + duration - std::chrono::seconds(sec)) + .count(); + callback(static_cast(sec), static_cast(usec)); +} + +template inline constexpr size_t str_len(const char (&)[N]) { + return N - 1; +} + +inline bool is_numeric(const std::string &str) { + return !str.empty() && + std::all_of(str.cbegin(), str.cend(), + [](unsigned char c) { return std::isdigit(c); }); +} + +inline size_t get_header_value_u64(const Headers &headers, + const std::string &key, size_t def, + size_t id, bool &is_invalid_value) { + is_invalid_value = false; + auto rng = headers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { + if (is_numeric(it->second)) { + return std::strtoull(it->second.data(), nullptr, 10); + } else { + is_invalid_value = true; + } + } + return def; +} + +inline size_t get_header_value_u64(const Headers &headers, + const std::string &key, size_t def, + size_t id) { + auto dummy = false; + return get_header_value_u64(headers, key, def, id, dummy); +} + +} // namespace detail + +inline size_t Request::get_header_value_u64(const std::string &key, size_t def, + size_t id) const { + return detail::get_header_value_u64(headers, key, def, id); +} + +inline size_t Response::get_header_value_u64(const std::string &key, size_t def, + size_t id) const { + return detail::get_header_value_u64(headers, key, def, id); +} + +namespace detail { + +inline bool set_socket_opt_impl(socket_t sock, int level, int optname, + const void *optval, socklen_t optlen) { + return setsockopt(sock, level, optname, +#ifdef _WIN32 + reinterpret_cast(optval), +#else + optval, +#endif + optlen) == 0; +} + +inline bool set_socket_opt(socket_t sock, int level, int optname, int optval) { + return set_socket_opt_impl(sock, level, optname, &optval, sizeof(optval)); +} + +inline bool set_socket_opt_time(socket_t sock, int level, int optname, + time_t sec, time_t usec) { +#ifdef _WIN32 + auto timeout = static_cast(sec * 1000 + usec / 1000); +#else + timeval timeout; + timeout.tv_sec = static_cast(sec); + timeout.tv_usec = static_cast(usec); +#endif + return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout)); +} + +} // namespace detail + +inline void default_socket_options(socket_t sock) { + detail::set_socket_opt(sock, SOL_SOCKET, +#ifdef SO_REUSEPORT + SO_REUSEPORT, +#else + SO_REUSEADDR, +#endif + 1); +} + +inline const char *status_message(int status) { + switch (status) { + case StatusCode::Continue_100: return "Continue"; + case StatusCode::SwitchingProtocol_101: return "Switching Protocol"; + case StatusCode::Processing_102: return "Processing"; + case StatusCode::EarlyHints_103: return "Early Hints"; + case StatusCode::OK_200: return "OK"; + case StatusCode::Created_201: return "Created"; + case StatusCode::Accepted_202: return "Accepted"; + case StatusCode::NonAuthoritativeInformation_203: + return "Non-Authoritative Information"; + case StatusCode::NoContent_204: return "No Content"; + case StatusCode::ResetContent_205: return "Reset Content"; + case StatusCode::PartialContent_206: return "Partial Content"; + case StatusCode::MultiStatus_207: return "Multi-Status"; + case StatusCode::AlreadyReported_208: return "Already Reported"; + case StatusCode::IMUsed_226: return "IM Used"; + case StatusCode::MultipleChoices_300: return "Multiple Choices"; + case StatusCode::MovedPermanently_301: return "Moved Permanently"; + case StatusCode::Found_302: return "Found"; + case StatusCode::SeeOther_303: return "See Other"; + case StatusCode::NotModified_304: return "Not Modified"; + case StatusCode::UseProxy_305: return "Use Proxy"; + case StatusCode::unused_306: return "unused"; + case StatusCode::TemporaryRedirect_307: return "Temporary Redirect"; + case StatusCode::PermanentRedirect_308: return "Permanent Redirect"; + case StatusCode::BadRequest_400: return "Bad Request"; + case StatusCode::Unauthorized_401: return "Unauthorized"; + case StatusCode::PaymentRequired_402: return "Payment Required"; + case StatusCode::Forbidden_403: return "Forbidden"; + case StatusCode::NotFound_404: return "Not Found"; + case StatusCode::MethodNotAllowed_405: return "Method Not Allowed"; + case StatusCode::NotAcceptable_406: return "Not Acceptable"; + case StatusCode::ProxyAuthenticationRequired_407: + return "Proxy Authentication Required"; + case StatusCode::RequestTimeout_408: return "Request Timeout"; + case StatusCode::Conflict_409: return "Conflict"; + case StatusCode::Gone_410: return "Gone"; + case StatusCode::LengthRequired_411: return "Length Required"; + case StatusCode::PreconditionFailed_412: return "Precondition Failed"; + case StatusCode::PayloadTooLarge_413: return "Payload Too Large"; + case StatusCode::UriTooLong_414: return "URI Too Long"; + case StatusCode::UnsupportedMediaType_415: return "Unsupported Media Type"; + case StatusCode::RangeNotSatisfiable_416: return "Range Not Satisfiable"; + case StatusCode::ExpectationFailed_417: return "Expectation Failed"; + case StatusCode::ImATeapot_418: return "I'm a teapot"; + case StatusCode::MisdirectedRequest_421: return "Misdirected Request"; + case StatusCode::UnprocessableContent_422: return "Unprocessable Content"; + case StatusCode::Locked_423: return "Locked"; + case StatusCode::FailedDependency_424: return "Failed Dependency"; + case StatusCode::TooEarly_425: return "Too Early"; + case StatusCode::UpgradeRequired_426: return "Upgrade Required"; + case StatusCode::PreconditionRequired_428: return "Precondition Required"; + case StatusCode::TooManyRequests_429: return "Too Many Requests"; + case StatusCode::RequestHeaderFieldsTooLarge_431: + return "Request Header Fields Too Large"; + case StatusCode::UnavailableForLegalReasons_451: + return "Unavailable For Legal Reasons"; + case StatusCode::NotImplemented_501: return "Not Implemented"; + case StatusCode::BadGateway_502: return "Bad Gateway"; + case StatusCode::ServiceUnavailable_503: return "Service Unavailable"; + case StatusCode::GatewayTimeout_504: return "Gateway Timeout"; + case StatusCode::HttpVersionNotSupported_505: + return "HTTP Version Not Supported"; + case StatusCode::VariantAlsoNegotiates_506: return "Variant Also Negotiates"; + case StatusCode::InsufficientStorage_507: return "Insufficient Storage"; + case StatusCode::LoopDetected_508: return "Loop Detected"; + case StatusCode::NotExtended_510: return "Not Extended"; + case StatusCode::NetworkAuthenticationRequired_511: + return "Network Authentication Required"; + + default: + case StatusCode::InternalServerError_500: return "Internal Server Error"; + } +} + +inline std::string get_bearer_token_auth(const Request &req) { + if (req.has_header("Authorization")) { + constexpr auto bearer_header_prefix_len = detail::str_len("Bearer "); + return req.get_header_value("Authorization") + .substr(bearer_header_prefix_len); + } + return ""; +} + +template +inline Server & +Server::set_read_timeout(const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); + return *this; +} + +template +inline Server & +Server::set_write_timeout(const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); + return *this; +} + +template +inline Server & +Server::set_idle_interval(const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); }); + return *this; +} + +inline std::string to_string(const Error error) { + switch (error) { + case Error::Success: return "Success (no error)"; + case Error::Unknown: return "Unknown"; + case Error::Connection: return "Could not establish connection"; + case Error::BindIPAddress: return "Failed to bind IP address"; + case Error::Read: return "Failed to read connection"; + case Error::Write: return "Failed to write connection"; + case Error::ExceedRedirectCount: return "Maximum redirect count exceeded"; + case Error::Canceled: return "Connection handling canceled"; + case Error::SSLConnection: return "SSL connection failed"; + case Error::SSLLoadingCerts: return "SSL certificate loading failed"; + case Error::SSLServerVerification: return "SSL server verification failed"; + case Error::SSLServerHostnameVerification: + return "SSL server hostname verification failed"; + case Error::UnsupportedMultipartBoundaryChars: + return "Unsupported HTTP multipart boundary characters"; + case Error::Compression: return "Compression failed"; + case Error::ConnectionTimeout: return "Connection timed out"; + case Error::ProxyConnection: return "Proxy connection failed"; + case Error::ResourceExhaustion: return "Resource exhaustion"; + case Error::TooManyFormDataFiles: return "Too many form data files"; + case Error::ExceedMaxPayloadSize: return "Exceeded maximum payload size"; + case Error::ExceedUriMaxLength: return "Exceeded maximum URI length"; + case Error::ExceedMaxSocketDescriptorCount: + return "Exceeded maximum socket descriptor count"; + case Error::InvalidRequestLine: return "Invalid request line"; + case Error::InvalidHTTPMethod: return "Invalid HTTP method"; + case Error::InvalidHTTPVersion: return "Invalid HTTP version"; + case Error::InvalidHeaders: return "Invalid headers"; + case Error::MultipartParsing: return "Multipart parsing failed"; + case Error::OpenFile: return "Failed to open file"; + case Error::Listen: return "Failed to listen on socket"; + case Error::GetSockName: return "Failed to get socket name"; + case Error::UnsupportedAddressFamily: return "Unsupported address family"; + case Error::HTTPParsing: return "HTTP parsing failed"; + case Error::InvalidRangeHeader: return "Invalid Range header"; + default: break; + } + + return "Invalid"; +} + +inline std::ostream &operator<<(std::ostream &os, const Error &obj) { + os << to_string(obj); + os << " (" << static_cast::type>(obj) << ')'; + return os; +} + +inline size_t Result::get_request_header_value_u64(const std::string &key, + size_t def, + size_t id) const { + return detail::get_header_value_u64(request_headers_, key, def, id); +} + +template +inline void ClientImpl::set_connection_timeout( + const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) { + set_connection_timeout(sec, usec); + }); +} + +template +inline void ClientImpl::set_read_timeout( + const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); +} + +template +inline void ClientImpl::set_write_timeout( + const std::chrono::duration &duration) { + detail::duration_to_sec_and_usec( + duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); +} + +template +inline void ClientImpl::set_max_timeout( + const std::chrono::duration &duration) { + auto msec = + std::chrono::duration_cast(duration).count(); + set_max_timeout(msec); +} + +template +inline void Client::set_connection_timeout( + const std::chrono::duration &duration) { + cli_->set_connection_timeout(duration); +} + +template +inline void +Client::set_read_timeout(const std::chrono::duration &duration) { + cli_->set_read_timeout(duration); +} + +template +inline void +Client::set_write_timeout(const std::chrono::duration &duration) { + cli_->set_write_timeout(duration); +} + +inline void Client::set_max_timeout(time_t msec) { + cli_->set_max_timeout(msec); +} + +template +inline void +Client::set_max_timeout(const std::chrono::duration &duration) { + cli_->set_max_timeout(duration); +} + +/* + * Forward declarations and types that will be part of the .h file if split into + * .h + .cc. + */ + +std::string hosted_at(const std::string &hostname); + +void hosted_at(const std::string &hostname, std::vector &addrs); + +// JavaScript-style URL encoding/decoding functions +std::string encode_uri_component(const std::string &value); +std::string encode_uri(const std::string &value); +std::string decode_uri_component(const std::string &value); +std::string decode_uri(const std::string &value); + +// RFC 3986 compliant URL component encoding/decoding functions +std::string encode_path_component(const std::string &component); +std::string decode_path_component(const std::string &component); +std::string encode_query_component(const std::string &component, + bool space_as_plus = true); +std::string decode_query_component(const std::string &component, + bool plus_as_space = true); + +std::string append_query_params(const std::string &path, const Params ¶ms); + +std::pair make_range_header(const Ranges &ranges); + +std::pair +make_basic_authentication_header(const std::string &username, + const std::string &password, + bool is_proxy = false); + +namespace detail { + +#if defined(_WIN32) +inline std::wstring u8string_to_wstring(const char *s) { + std::wstring ws; + auto len = static_cast(strlen(s)); + auto wlen = ::MultiByteToWideChar(CP_UTF8, 0, s, len, nullptr, 0); + if (wlen > 0) { + ws.resize(wlen); + wlen = ::MultiByteToWideChar( + CP_UTF8, 0, s, len, + const_cast(reinterpret_cast(ws.data())), wlen); + if (wlen != static_cast(ws.size())) { ws.clear(); } + } + return ws; +} +#endif + +struct FileStat { + FileStat(const std::string &path); + bool is_file() const; + bool is_dir() const; + +private: +#if defined(_WIN32) + struct _stat st_; +#else + struct stat st_; +#endif + int ret_ = -1; +}; + +std::string trim_copy(const std::string &s); + +void divide( + const char *data, std::size_t size, char d, + std::function + fn); + +void divide( + const std::string &str, char d, + std::function + fn); + +void split(const char *b, const char *e, char d, + std::function fn); + +void split(const char *b, const char *e, char d, size_t m, + std::function fn); + +bool process_client_socket( + socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + time_t max_timeout_msec, + std::chrono::time_point start_time, + std::function callback); + +socket_t create_client_socket(const std::string &host, const std::string &ip, + int port, int address_family, bool tcp_nodelay, + bool ipv6_v6only, SocketOptions socket_options, + time_t connection_timeout_sec, + time_t connection_timeout_usec, + time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec, + const std::string &intf, Error &error); + +const char *get_header_value(const Headers &headers, const std::string &key, + const char *def, size_t id); + +std::string params_to_query_str(const Params ¶ms); + +void parse_query_text(const char *data, std::size_t size, Params ¶ms); + +void parse_query_text(const std::string &s, Params ¶ms); + +bool parse_multipart_boundary(const std::string &content_type, + std::string &boundary); + +bool parse_range_header(const std::string &s, Ranges &ranges); + +bool parse_accept_header(const std::string &s, + std::vector &content_types); + +int close_socket(socket_t sock); + +ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags); + +ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags); + +enum class EncodingType { None = 0, Gzip, Brotli, Zstd }; + +EncodingType encoding_type(const Request &req, const Response &res); + +class BufferStream final : public Stream { +public: + BufferStream() = default; + ~BufferStream() override = default; + + bool is_readable() const override; + bool wait_readable() const override; + bool wait_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + void get_local_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + time_t duration() const override; + + const std::string &get_buffer() const; + +private: + std::string buffer; + size_t position = 0; +}; + +class compressor { +public: + virtual ~compressor() = default; + + typedef std::function Callback; + virtual bool compress(const char *data, size_t data_length, bool last, + Callback callback) = 0; +}; + +class decompressor { +public: + virtual ~decompressor() = default; + + virtual bool is_valid() const = 0; + + typedef std::function Callback; + virtual bool decompress(const char *data, size_t data_length, + Callback callback) = 0; +}; + +class nocompressor final : public compressor { +public: + ~nocompressor() override = default; + + bool compress(const char *data, size_t data_length, bool /*last*/, + Callback callback) override; +}; + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +class gzip_compressor final : public compressor { +public: + gzip_compressor(); + ~gzip_compressor() override; + + bool compress(const char *data, size_t data_length, bool last, + Callback callback) override; + +private: + bool is_valid_ = false; + z_stream strm_; +}; + +class gzip_decompressor final : public decompressor { +public: + gzip_decompressor(); + ~gzip_decompressor() override; + + bool is_valid() const override; + + bool decompress(const char *data, size_t data_length, + Callback callback) override; + +private: + bool is_valid_ = false; + z_stream strm_; +}; +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +class brotli_compressor final : public compressor { +public: + brotli_compressor(); + ~brotli_compressor(); + + bool compress(const char *data, size_t data_length, bool last, + Callback callback) override; + +private: + BrotliEncoderState *state_ = nullptr; +}; + +class brotli_decompressor final : public decompressor { +public: + brotli_decompressor(); + ~brotli_decompressor(); + + bool is_valid() const override; + + bool decompress(const char *data, size_t data_length, + Callback callback) override; + +private: + BrotliDecoderResult decoder_r; + BrotliDecoderState *decoder_s = nullptr; +}; +#endif + +#ifdef CPPHTTPLIB_ZSTD_SUPPORT +class zstd_compressor : public compressor { +public: + zstd_compressor(); + ~zstd_compressor(); + + bool compress(const char *data, size_t data_length, bool last, + Callback callback) override; + +private: + ZSTD_CCtx *ctx_ = nullptr; +}; + +class zstd_decompressor : public decompressor { +public: + zstd_decompressor(); + ~zstd_decompressor(); + + bool is_valid() const override; + + bool decompress(const char *data, size_t data_length, + Callback callback) override; + +private: + ZSTD_DCtx *ctx_ = nullptr; +}; +#endif + +// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` +// to store data. The call can set memory on stack for performance. +class stream_line_reader { +public: + stream_line_reader(Stream &strm, char *fixed_buffer, + size_t fixed_buffer_size); + const char *ptr() const; + size_t size() const; + bool end_with_crlf() const; + bool getline(); + +private: + void append(char c); + + Stream &strm_; + char *fixed_buffer_; + const size_t fixed_buffer_size_; + size_t fixed_buffer_used_size_ = 0; + std::string growable_buffer_; +}; + +bool parse_trailers(stream_line_reader &line_reader, Headers &dest, + const Headers &src_headers); + +struct ChunkedDecoder { + Stream &strm; + size_t chunk_remaining = 0; + bool finished = false; + char line_buf[64]; + size_t last_chunk_total = 0; + size_t last_chunk_offset = 0; + + explicit ChunkedDecoder(Stream &s); + + ssize_t read_payload(char *buf, size_t len, size_t &out_chunk_offset, + size_t &out_chunk_total); + + bool parse_trailers_into(Headers &dest, const Headers &src_headers); +}; + +class mmap { +public: + mmap(const char *path); + ~mmap(); + + bool open(const char *path); + void close(); + + bool is_open() const; + size_t size() const; + const char *data() const; + +private: +#if defined(_WIN32) + HANDLE hFile_ = NULL; + HANDLE hMapping_ = NULL; +#else + int fd_ = -1; +#endif + size_t size_ = 0; + void *addr_ = nullptr; + bool is_open_empty_file = false; +}; + +// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 +namespace fields { + +inline bool is_token_char(char c) { + return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' || + c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || + c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; +} + +inline bool is_token(const std::string &s) { + if (s.empty()) { return false; } + for (auto c : s) { + if (!is_token_char(c)) { return false; } + } + return true; +} + +inline bool is_field_name(const std::string &s) { return is_token(s); } + +inline bool is_vchar(char c) { return c >= 33 && c <= 126; } + +inline bool is_obs_text(char c) { return 128 <= static_cast(c); } + +inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); } + +inline bool is_field_content(const std::string &s) { + if (s.empty()) { return true; } + + if (s.size() == 1) { + return is_field_vchar(s[0]); + } else if (s.size() == 2) { + return is_field_vchar(s[0]) && is_field_vchar(s[1]); + } else { + size_t i = 0; + + if (!is_field_vchar(s[i])) { return false; } + i++; + + while (i < s.size() - 1) { + auto c = s[i++]; + if (c == ' ' || c == '\t' || is_field_vchar(c)) { + } else { + return false; + } + } + + return is_field_vchar(s[i]); + } +} + +inline bool is_field_value(const std::string &s) { return is_field_content(s); } + +} // namespace fields + +} // namespace detail + +// ---------------------------------------------------------------------------- + +/* + * Implementation that will be part of the .cc file if split into .h + .cc. + */ + +namespace detail { + +inline bool is_hex(char c, int &v) { + if (0x20 <= c && isdigit(c)) { + v = c - '0'; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } + return false; +} + +inline bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, + int &val) { + if (i >= s.size()) { return false; } + + val = 0; + for (; cnt; i++, cnt--) { + if (!s[i]) { return false; } + auto v = 0; + if (is_hex(s[i], v)) { + val = val * 16 + v; + } else { + return false; + } + } + return true; +} + +inline std::string from_i_to_hex(size_t n) { + static const auto charset = "0123456789abcdef"; + std::string ret; + do { + ret = charset[n & 15] + ret; + n >>= 4; + } while (n > 0); + return ret; +} + +inline size_t to_utf8(int code, char *buff) { + if (code < 0x0080) { + buff[0] = static_cast(code & 0x7F); + return 1; + } else if (code < 0x0800) { + buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); + buff[1] = static_cast(0x80 | (code & 0x3F)); + return 2; + } else if (code < 0xD800) { + buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (code & 0x3F)); + return 3; + } else if (code < 0xE000) { // D800 - DFFF is invalid... + return 0; + } else if (code < 0x10000) { + buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (code & 0x3F)); + return 3; + } else if (code < 0x110000) { + buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); + buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); + buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); + buff[3] = static_cast(0x80 | (code & 0x3F)); + return 4; + } + + // NOTREACHED + return 0; +} + +// NOTE: This code came up with the following stackoverflow post: +// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c +inline std::string base64_encode(const std::string &in) { + static const auto lookup = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string out; + out.reserve(in.size()); + + auto val = 0; + auto valb = -6; + + for (auto c : in) { + val = (val << 8) + static_cast(c); + valb += 8; + while (valb >= 0) { + out.push_back(lookup[(val >> valb) & 0x3F]); + valb -= 6; + } + } + + if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } + + while (out.size() % 4) { + out.push_back('='); + } + + return out; +} + +inline bool is_valid_path(const std::string &path) { + size_t level = 0; + size_t i = 0; + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + + while (i < path.size()) { + // Read component + auto beg = i; + while (i < path.size() && path[i] != '/') { + if (path[i] == '\0') { + return false; + } else if (path[i] == '\\') { + return false; + } + i++; + } + + auto len = i - beg; + assert(len > 0); + + if (!path.compare(beg, len, ".")) { + ; + } else if (!path.compare(beg, len, "..")) { + if (level == 0) { return false; } + level--; + } else { + level++; + } + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + } + + return true; +} + +inline FileStat::FileStat(const std::string &path) { +#if defined(_WIN32) + auto wpath = u8string_to_wstring(path.c_str()); + ret_ = _wstat(wpath.c_str(), &st_); +#else + ret_ = stat(path.c_str(), &st_); +#endif +} +inline bool FileStat::is_file() const { + return ret_ >= 0 && S_ISREG(st_.st_mode); +} +inline bool FileStat::is_dir() const { + return ret_ >= 0 && S_ISDIR(st_.st_mode); +} + +inline std::string encode_path(const std::string &s) { + std::string result; + result.reserve(s.size()); + + for (size_t i = 0; s[i]; i++) { + switch (s[i]) { + case ' ': result += "%20"; break; + case '+': result += "%2B"; break; + case '\r': result += "%0D"; break; + case '\n': result += "%0A"; break; + case '\'': result += "%27"; break; + case ',': result += "%2C"; break; + // case ':': result += "%3A"; break; // ok? probably... + case ';': result += "%3B"; break; + default: + auto c = static_cast(s[i]); + if (c >= 0x80) { + result += '%'; + char hex[4]; + auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); + assert(len == 2); + result.append(hex, static_cast(len)); + } else { + result += s[i]; + } + break; + } + } + + return result; +} + +inline std::string file_extension(const std::string &path) { + std::smatch m; + thread_local auto re = std::regex("\\.([a-zA-Z0-9]+)$"); + if (std::regex_search(path, m, re)) { return m[1].str(); } + return std::string(); +} + +inline bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } + +template +inline bool parse_header(const char *beg, const char *end, T fn); + +template +inline bool parse_header(const char *beg, const char *end, T fn) { + // Skip trailing spaces and tabs. + while (beg < end && is_space_or_tab(end[-1])) { + end--; + } + + auto p = beg; + while (p < end && *p != ':') { + p++; + } + + auto name = std::string(beg, p); + if (!detail::fields::is_field_name(name)) { return false; } + + if (p == end) { return false; } + + auto key_end = p; + + if (*p++ != ':') { return false; } + + while (p < end && is_space_or_tab(*p)) { + p++; + } + + if (p <= end) { + auto key_len = key_end - beg; + if (!key_len) { return false; } + + auto key = std::string(beg, key_end); + auto val = std::string(p, end); + + if (!detail::fields::is_field_value(val)) { return false; } + + if (case_ignore::equal(key, "Location") || + case_ignore::equal(key, "Referer")) { + fn(key, val); + } else { + fn(key, decode_path_component(val)); + } + + return true; + } + + return false; +} + +inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest, + const Headers &src_headers) { + // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions "The chunked + // transfer coding is complete when a chunk with a chunk-size of zero is + // received, possibly followed by a trailer section, and finally terminated by + // an empty line". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1 + // + // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section + // doesn't care for the existence of the final CRLF. In other words, it seems + // to be ok whether the final CRLF exists or not in the chunked data. + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3 + // + // According to the reference code in RFC 9112, cpp-httplib now allows + // chunked transfer coding data without the final CRLF. + + // RFC 7230 Section 4.1.2 - Headers prohibited in trailers + thread_local case_ignore::unordered_set prohibited_trailers = { + "transfer-encoding", + "content-length", + "host", + "authorization", + "www-authenticate", + "proxy-authenticate", + "proxy-authorization", + "cookie", + "set-cookie", + "cache-control", + "expect", + "max-forwards", + "pragma", + "range", + "te", + "age", + "expires", + "date", + "location", + "retry-after", + "vary", + "warning", + "content-encoding", + "content-type", + "content-range", + "trailer"}; + + case_ignore::unordered_set declared_trailers; + auto trailer_header = get_header_value(src_headers, "Trailer", "", 0); + if (trailer_header && std::strlen(trailer_header)) { + auto len = std::strlen(trailer_header); + split(trailer_header, trailer_header + len, ',', + [&](const char *b, const char *e) { + const char *kbeg = b; + const char *kend = e; + while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) { + ++kbeg; + } + while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) { + --kend; + } + std::string key(kbeg, static_cast(kend - kbeg)); + if (!key.empty() && + prohibited_trailers.find(key) == prohibited_trailers.end()) { + declared_trailers.insert(key); + } + }); + } + + size_t trailer_header_count = 0; + while (strcmp(line_reader.ptr(), "\r\n") != 0) { + if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } + + constexpr auto line_terminator_len = 2; + auto line_beg = line_reader.ptr(); + auto line_end = + line_reader.ptr() + line_reader.size() - line_terminator_len; + + if (!parse_header(line_beg, line_end, + [&](const std::string &key, const std::string &val) { + if (declared_trailers.find(key) != + declared_trailers.end()) { + dest.emplace(key, val); + trailer_header_count++; + } + })) { + return false; + } + + if (!line_reader.getline()) { return false; } + } + + return true; +} + +inline std::pair trim(const char *b, const char *e, size_t left, + size_t right) { + while (b + left < e && is_space_or_tab(b[left])) { + left++; + } + while (right > 0 && is_space_or_tab(b[right - 1])) { + right--; + } + return std::make_pair(left, right); +} + +inline std::string trim_copy(const std::string &s) { + auto r = trim(s.data(), s.data() + s.size(), 0, s.size()); + return s.substr(r.first, r.second - r.first); +} + +inline std::string trim_double_quotes_copy(const std::string &s) { + if (s.length() >= 2 && s.front() == '"' && s.back() == '"') { + return s.substr(1, s.size() - 2); + } + return s; +} + +inline void +divide(const char *data, std::size_t size, char d, + std::function + fn) { + const auto it = std::find(data, data + size, d); + const auto found = static_cast(it != data + size); + const auto lhs_data = data; + const auto lhs_size = static_cast(it - data); + const auto rhs_data = it + found; + const auto rhs_size = size - lhs_size - found; + + fn(lhs_data, lhs_size, rhs_data, rhs_size); +} + +inline void +divide(const std::string &str, char d, + std::function + fn) { + divide(str.data(), str.size(), d, std::move(fn)); +} + +inline void split(const char *b, const char *e, char d, + std::function fn) { + return split(b, e, d, (std::numeric_limits::max)(), std::move(fn)); +} + +inline void split(const char *b, const char *e, char d, size_t m, + std::function fn) { + size_t i = 0; + size_t beg = 0; + size_t count = 1; + + while (e ? (b + i < e) : (b[i] != '\0')) { + if (b[i] == d && count < m) { + auto r = trim(b, e, beg, i); + if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } + beg = i + 1; + count++; + } + i++; + } + + if (i) { + auto r = trim(b, e, beg, i); + if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } + } +} + +inline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer, + size_t fixed_buffer_size) + : strm_(strm), fixed_buffer_(fixed_buffer), + fixed_buffer_size_(fixed_buffer_size) {} + +inline const char *stream_line_reader::ptr() const { + if (growable_buffer_.empty()) { + return fixed_buffer_; + } else { + return growable_buffer_.data(); + } +} + +inline size_t stream_line_reader::size() const { + if (growable_buffer_.empty()) { + return fixed_buffer_used_size_; + } else { + return growable_buffer_.size(); + } +} + +inline bool stream_line_reader::end_with_crlf() const { + auto end = ptr() + size(); + return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; +} + +inline bool stream_line_reader::getline() { + fixed_buffer_used_size_ = 0; + growable_buffer_.clear(); + +#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + char prev_byte = 0; +#endif + + for (size_t i = 0;; i++) { + if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) { + // Treat exceptionally long lines as an error to + // prevent infinite loops/memory exhaustion + return false; + } + char byte; + auto n = strm_.read(&byte, 1); + + if (n < 0) { + return false; + } else if (n == 0) { + if (i == 0) { + return false; + } else { + break; + } + } + + append(byte); + +#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + if (byte == '\n') { break; } +#else + if (prev_byte == '\r' && byte == '\n') { break; } + prev_byte = byte; +#endif + } + + return true; +} + +inline void stream_line_reader::append(char c) { + if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { + fixed_buffer_[fixed_buffer_used_size_++] = c; + fixed_buffer_[fixed_buffer_used_size_] = '\0'; + } else { + if (growable_buffer_.empty()) { + assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); + growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); + } + growable_buffer_ += c; + } +} + +inline mmap::mmap(const char *path) { open(path); } + +inline mmap::~mmap() { close(); } + +inline bool mmap::open(const char *path) { + close(); + +#if defined(_WIN32) + auto wpath = u8string_to_wstring(path); + if (wpath.empty()) { return false; } + + hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ, + OPEN_EXISTING, NULL); + + if (hFile_ == INVALID_HANDLE_VALUE) { return false; } + + LARGE_INTEGER size{}; + if (!::GetFileSizeEx(hFile_, &size)) { return false; } + // If the following line doesn't compile due to QuadPart, update Windows SDK. + // See: + // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721 + if (static_cast(size.QuadPart) > + (std::numeric_limits::max)()) { + // `size_t` might be 32-bits, on 32-bits Windows. + return false; + } + size_ = static_cast(size.QuadPart); + + hMapping_ = + ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL); + + // Special treatment for an empty file... + if (hMapping_ == NULL && size_ == 0) { + close(); + is_open_empty_file = true; + return true; + } + + if (hMapping_ == NULL) { + close(); + return false; + } + + addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0); + + if (addr_ == nullptr) { + close(); + return false; + } +#else + fd_ = ::open(path, O_RDONLY); + if (fd_ == -1) { return false; } + + struct stat sb; + if (fstat(fd_, &sb) == -1) { + close(); + return false; + } + size_ = static_cast(sb.st_size); + + addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0); + + // Special treatment for an empty file... + if (addr_ == MAP_FAILED && size_ == 0) { + close(); + is_open_empty_file = true; + return false; + } +#endif + + return true; +} + +inline bool mmap::is_open() const { + return is_open_empty_file ? true : addr_ != nullptr; +} + +inline size_t mmap::size() const { return size_; } + +inline const char *mmap::data() const { + return is_open_empty_file ? "" : static_cast(addr_); +} + +inline void mmap::close() { +#if defined(_WIN32) + if (addr_) { + ::UnmapViewOfFile(addr_); + addr_ = nullptr; + } + + if (hMapping_) { + ::CloseHandle(hMapping_); + hMapping_ = NULL; + } + + if (hFile_ != INVALID_HANDLE_VALUE) { + ::CloseHandle(hFile_); + hFile_ = INVALID_HANDLE_VALUE; + } + + is_open_empty_file = false; +#else + if (addr_ != nullptr) { + munmap(addr_, size_); + addr_ = nullptr; + } + + if (fd_ != -1) { + ::close(fd_); + fd_ = -1; + } +#endif + size_ = 0; +} +inline int close_socket(socket_t sock) { +#ifdef _WIN32 + return closesocket(sock); +#else + return close(sock); +#endif +} + +template inline ssize_t handle_EINTR(T fn) { + ssize_t res = 0; + while (true) { + res = fn(); + if (res < 0 && errno == EINTR) { + std::this_thread::sleep_for(std::chrono::microseconds{1}); + continue; + } + break; + } + return res; +} + +inline ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) { + return handle_EINTR([&]() { + return recv(sock, +#ifdef _WIN32 + static_cast(ptr), static_cast(size), +#else + ptr, size, +#endif + flags); + }); +} + +inline ssize_t send_socket(socket_t sock, const void *ptr, size_t size, + int flags) { + return handle_EINTR([&]() { + return send(sock, +#ifdef _WIN32 + static_cast(ptr), static_cast(size), +#else + ptr, size, +#endif + flags); + }); +} + +inline int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) { +#ifdef _WIN32 + return ::WSAPoll(fds, nfds, timeout); +#else + return ::poll(fds, nfds, timeout); +#endif +} + +template +inline ssize_t select_impl(socket_t sock, time_t sec, time_t usec) { +#ifdef __APPLE__ + if (sock >= FD_SETSIZE) { return -1; } + + fd_set fds, *rfds, *wfds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + rfds = (Read ? &fds : nullptr); + wfds = (Read ? nullptr : &fds); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + return handle_EINTR([&]() { + return select(static_cast(sock + 1), rfds, wfds, nullptr, &tv); + }); +#else + struct pollfd pfd; + pfd.fd = sock; + pfd.events = (Read ? POLLIN : POLLOUT); + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); }); +#endif +} + +inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) { + return select_impl(sock, sec, usec); +} + +inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) { + return select_impl(sock, sec, usec); +} + +inline Error wait_until_socket_is_ready(socket_t sock, time_t sec, + time_t usec) { +#ifdef __APPLE__ + if (sock >= FD_SETSIZE) { return Error::Connection; } + + fd_set fdsr, fdsw; + FD_ZERO(&fdsr); + FD_ZERO(&fdsw); + FD_SET(sock, &fdsr); + FD_SET(sock, &fdsw); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + auto ret = handle_EINTR([&]() { + return select(static_cast(sock + 1), &fdsr, &fdsw, nullptr, &tv); + }); + + if (ret == 0) { return Error::ConnectionTimeout; } + + if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { + auto error = 0; + socklen_t len = sizeof(error); + auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&error), &len); + auto successful = res >= 0 && !error; + return successful ? Error::Success : Error::Connection; + } + + return Error::Connection; +#else + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLIN | POLLOUT; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + auto poll_res = + handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); }); + + if (poll_res == 0) { return Error::ConnectionTimeout; } + + if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { + auto error = 0; + socklen_t len = sizeof(error); + auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&error), &len); + auto successful = res >= 0 && !error; + return successful ? Error::Success : Error::Connection; + } + + return Error::Connection; +#endif +} + +inline bool is_socket_alive(socket_t sock) { + const auto val = detail::select_read(sock, 0, 0); + if (val == 0) { + return true; + } else if (val < 0 && errno == EBADF) { + return false; + } + char buf[1]; + return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0; +} + +class SocketStream final : public Stream { +public: + SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + time_t max_timeout_msec = 0, + std::chrono::time_point start_time = + (std::chrono::steady_clock::time_point::min)()); + ~SocketStream() override; + + bool is_readable() const override; + bool wait_readable() const override; + bool wait_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + void get_local_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + time_t duration() const override; + +private: + socket_t sock_; + time_t read_timeout_sec_; + time_t read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; + time_t max_timeout_msec_; + const std::chrono::time_point start_time_; + + std::vector read_buff_; + size_t read_buff_off_ = 0; + size_t read_buff_content_size_ = 0; + + static const size_t read_buff_size_ = 1024l * 4; +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLSocketStream final : public Stream { +public: + SSLSocketStream( + socket_t sock, SSL *ssl, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, time_t max_timeout_msec = 0, + std::chrono::time_point start_time = + (std::chrono::steady_clock::time_point::min)()); + ~SSLSocketStream() override; + + bool is_readable() const override; + bool wait_readable() const override; + bool wait_writable() const override; + ssize_t read(char *ptr, size_t size) override; + ssize_t write(const char *ptr, size_t size) override; + void get_remote_ip_and_port(std::string &ip, int &port) const override; + void get_local_ip_and_port(std::string &ip, int &port) const override; + socket_t socket() const override; + time_t duration() const override; + +private: + socket_t sock_; + SSL *ssl_; + time_t read_timeout_sec_; + time_t read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; + time_t max_timeout_msec_; + const std::chrono::time_point start_time_; +}; +#endif + +inline bool keep_alive(const std::atomic &svr_sock, socket_t sock, + time_t keep_alive_timeout_sec) { + using namespace std::chrono; + + const auto interval_usec = + CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND; + + // Avoid expensive `steady_clock::now()` call for the first time + if (select_read(sock, 0, interval_usec) > 0) { return true; } + + const auto start = steady_clock::now() - microseconds{interval_usec}; + const auto timeout = seconds{keep_alive_timeout_sec}; + + while (true) { + if (svr_sock == INVALID_SOCKET) { + break; // Server socket is closed + } + + auto val = select_read(sock, 0, interval_usec); + if (val < 0) { + break; // Ssocket error + } else if (val == 0) { + if (steady_clock::now() - start > timeout) { + break; // Timeout + } + } else { + return true; // Ready for read + } + } + + return false; +} + +template +inline bool +process_server_socket_core(const std::atomic &svr_sock, socket_t sock, + size_t keep_alive_max_count, + time_t keep_alive_timeout_sec, T callback) { + assert(keep_alive_max_count > 0); + auto ret = false; + auto count = keep_alive_max_count; + while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { + auto close_connection = count == 1; + auto connection_closed = false; + ret = callback(close_connection, connection_closed); + if (!ret || connection_closed) { break; } + count--; + } + return ret; +} + +template +inline bool +process_server_socket(const std::atomic &svr_sock, socket_t sock, + size_t keep_alive_max_count, + time_t keep_alive_timeout_sec, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, T callback) { + return process_server_socket_core( + svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, + [&](bool close_connection, bool &connection_closed) { + SocketStream strm(sock, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm, close_connection, connection_closed); + }); +} + +inline bool process_client_socket( + socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + time_t max_timeout_msec, + std::chrono::time_point start_time, + std::function callback) { + SocketStream strm(sock, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec, max_timeout_msec, + start_time); + return callback(strm); +} + +inline int shutdown_socket(socket_t sock) { +#ifdef _WIN32 + return shutdown(sock, SD_BOTH); +#else + return shutdown(sock, SHUT_RDWR); +#endif +} + +inline std::string escape_abstract_namespace_unix_domain(const std::string &s) { + if (s.size() > 1 && s[0] == '\0') { + auto ret = s; + ret[0] = '@'; + return ret; + } + return s; +} + +inline std::string +unescape_abstract_namespace_unix_domain(const std::string &s) { + if (s.size() > 1 && s[0] == '@') { + auto ret = s; + ret[0] = '\0'; + return ret; + } + return s; +} + +inline int getaddrinfo_with_timeout(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res, time_t timeout_sec) { +#ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO + if (timeout_sec <= 0) { + // No timeout specified, use standard getaddrinfo + return getaddrinfo(node, service, hints, res); + } + +#ifdef _WIN32 + // Windows-specific implementation using GetAddrInfoEx with overlapped I/O + OVERLAPPED overlapped = {0}; + HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!event) { return EAI_FAIL; } + + overlapped.hEvent = event; + + PADDRINFOEXW result_addrinfo = nullptr; + HANDLE cancel_handle = nullptr; + + ADDRINFOEXW hints_ex = {0}; + if (hints) { + hints_ex.ai_flags = hints->ai_flags; + hints_ex.ai_family = hints->ai_family; + hints_ex.ai_socktype = hints->ai_socktype; + hints_ex.ai_protocol = hints->ai_protocol; + } + + auto wnode = u8string_to_wstring(node); + auto wservice = u8string_to_wstring(service); + + auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr, + hints ? &hints_ex : nullptr, &result_addrinfo, + nullptr, &overlapped, nullptr, &cancel_handle); + + if (ret == WSA_IO_PENDING) { + auto wait_result = + ::WaitForSingleObject(event, static_cast(timeout_sec * 1000)); + if (wait_result == WAIT_TIMEOUT) { + if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); } + ::CloseHandle(event); + return EAI_AGAIN; + } + + DWORD bytes_returned; + if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, + &bytes_returned, FALSE)) { + ::CloseHandle(event); + return ::WSAGetLastError(); + } + } + + ::CloseHandle(event); + + if (ret == NO_ERROR || ret == WSA_IO_PENDING) { + *res = reinterpret_cast(result_addrinfo); + return 0; + } + + return ret; +#elif TARGET_OS_MAC + // macOS implementation using CFHost API for asynchronous DNS resolution + CFStringRef hostname_ref = CFStringCreateWithCString( + kCFAllocatorDefault, node, kCFStringEncodingUTF8); + if (!hostname_ref) { return EAI_MEMORY; } + + CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref); + CFRelease(hostname_ref); + if (!host_ref) { return EAI_MEMORY; } + + // Set up context for callback + struct CFHostContext { + bool completed = false; + bool success = false; + CFArrayRef addresses = nullptr; + std::mutex mutex; + std::condition_variable cv; + } context; + + CFHostClientContext client_context; + memset(&client_context, 0, sizeof(client_context)); + client_context.info = &context; + + // Set callback + auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/, + const CFStreamError *error, void *info) { + auto ctx = static_cast(info); + std::lock_guard lock(ctx->mutex); + + if (error && error->error != 0) { + ctx->success = false; + } else { + Boolean hasBeenResolved; + ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved); + if (ctx->addresses && hasBeenResolved) { + CFRetain(ctx->addresses); + ctx->success = true; + } else { + ctx->success = false; + } + } + ctx->completed = true; + ctx->cv.notify_one(); + }; + + if (!CFHostSetClient(host_ref, callback, &client_context)) { + CFRelease(host_ref); + return EAI_SYSTEM; + } + + // Schedule on run loop + CFRunLoopRef run_loop = CFRunLoopGetCurrent(); + CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); + + // Start resolution + CFStreamError stream_error; + if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) { + CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); + CFRelease(host_ref); + return EAI_FAIL; + } + + // Wait for completion with timeout + auto timeout_time = + std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec); + bool timed_out = false; + + { + std::unique_lock lock(context.mutex); + + while (!context.completed) { + auto now = std::chrono::steady_clock::now(); + if (now >= timeout_time) { + timed_out = true; + break; + } + + // Run the runloop for a short time + lock.unlock(); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); + lock.lock(); + } + } + + // Clean up + CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); + CFHostSetClient(host_ref, nullptr, nullptr); + + if (timed_out || !context.completed) { + CFHostCancelInfoResolution(host_ref, kCFHostAddresses); + CFRelease(host_ref); + return EAI_AGAIN; + } + + if (!context.success || !context.addresses) { + CFRelease(host_ref); + return EAI_NODATA; + } + + // Convert CFArray to addrinfo + CFIndex count = CFArrayGetCount(context.addresses); + if (count == 0) { + CFRelease(context.addresses); + CFRelease(host_ref); + return EAI_NODATA; + } + + struct addrinfo *result_addrinfo = nullptr; + struct addrinfo **current = &result_addrinfo; + + for (CFIndex i = 0; i < count; i++) { + CFDataRef addr_data = + static_cast(CFArrayGetValueAtIndex(context.addresses, i)); + if (!addr_data) continue; + + const struct sockaddr *sockaddr_ptr = + reinterpret_cast(CFDataGetBytePtr(addr_data)); + socklen_t sockaddr_len = static_cast(CFDataGetLength(addr_data)); + + // Allocate addrinfo structure + *current = static_cast(malloc(sizeof(struct addrinfo))); + if (!*current) { + freeaddrinfo(result_addrinfo); + CFRelease(context.addresses); + CFRelease(host_ref); + return EAI_MEMORY; + } + + memset(*current, 0, sizeof(struct addrinfo)); + + // Set up addrinfo fields + (*current)->ai_family = sockaddr_ptr->sa_family; + (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM; + (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP; + (*current)->ai_addrlen = sockaddr_len; + + // Copy sockaddr + (*current)->ai_addr = static_cast(malloc(sockaddr_len)); + if (!(*current)->ai_addr) { + freeaddrinfo(result_addrinfo); + CFRelease(context.addresses); + CFRelease(host_ref); + return EAI_MEMORY; + } + memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len); + + // Set port if service is specified + if (service && strlen(service) > 0) { + int port = atoi(service); + if (port > 0) { + if (sockaddr_ptr->sa_family == AF_INET) { + reinterpret_cast((*current)->ai_addr) + ->sin_port = htons(static_cast(port)); + } else if (sockaddr_ptr->sa_family == AF_INET6) { + reinterpret_cast((*current)->ai_addr) + ->sin6_port = htons(static_cast(port)); + } + } + } + + current = &((*current)->ai_next); + } + + CFRelease(context.addresses); + CFRelease(host_ref); + + *res = result_addrinfo; + return 0; +#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \ + (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) + // Linux implementation using getaddrinfo_a for asynchronous DNS resolution + struct gaicb request; + struct gaicb *requests[1] = {&request}; + struct sigevent sevp; + struct timespec timeout; + + // Initialize the request structure + memset(&request, 0, sizeof(request)); + request.ar_name = node; + request.ar_service = service; + request.ar_request = hints; + + // Set up timeout + timeout.tv_sec = timeout_sec; + timeout.tv_nsec = 0; + + // Initialize sigevent structure (not used, but required) + memset(&sevp, 0, sizeof(sevp)); + sevp.sigev_notify = SIGEV_NONE; + + // Start asynchronous resolution + int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp); + if (start_result != 0) { return start_result; } + + // Wait for completion with timeout + int wait_result = + gai_suspend((const struct gaicb *const *)requests, 1, &timeout); + + if (wait_result == 0 || wait_result == EAI_ALLDONE) { + // Completed successfully, get the result + int gai_result = gai_error(&request); + if (gai_result == 0) { + *res = request.ar_result; + return 0; + } else { + // Clean up on error + if (request.ar_result) { freeaddrinfo(request.ar_result); } + return gai_result; + } + } else if (wait_result == EAI_AGAIN) { + // Timeout occurred, cancel the request + gai_cancel(&request); + return EAI_AGAIN; + } else { + // Other error occurred + gai_cancel(&request); + return wait_result; + } +#else + // Fallback implementation using thread-based timeout for other Unix systems + + struct GetAddrInfoState { + ~GetAddrInfoState() { + if (info) { freeaddrinfo(info); } + } + + std::mutex mutex; + std::condition_variable result_cv; + bool completed = false; + int result = EAI_SYSTEM; + std::string node; + std::string service; + struct addrinfo hints; + struct addrinfo *info = nullptr; + }; + + // Allocate on the heap, so the resolver thread can keep using the data. + auto state = std::make_shared(); + state->node = node; + state->service = service; + state->hints = *hints; + + std::thread resolve_thread([state]() { + auto thread_result = + getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints, + &state->info); + + std::lock_guard lock(state->mutex); + state->result = thread_result; + state->completed = true; + state->result_cv.notify_one(); + }); + + // Wait for completion or timeout + std::unique_lock lock(state->mutex); + auto finished = + state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), + [&] { return state->completed; }); + + if (finished) { + // Operation completed within timeout + resolve_thread.join(); + *res = state->info; + state->info = nullptr; // Pass ownership to caller + return state->result; + } else { + // Timeout occurred + resolve_thread.detach(); // Let the thread finish in background + return EAI_AGAIN; // Return timeout error + } +#endif +#else + (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo + return getaddrinfo(node, service, hints, res); +#endif +} + +template +socket_t create_socket(const std::string &host, const std::string &ip, int port, + int address_family, int socket_flags, bool tcp_nodelay, + bool ipv6_v6only, SocketOptions socket_options, + BindOrConnect bind_or_connect, time_t timeout_sec = 0) { + // Get address info + const char *node = nullptr; + struct addrinfo hints; + struct addrinfo *result; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_IP; + + if (!ip.empty()) { + node = ip.c_str(); + // Ask getaddrinfo to convert IP in c-string to address + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_NUMERICHOST; + } else { + if (!host.empty()) { node = host.c_str(); } + hints.ai_family = address_family; + hints.ai_flags = socket_flags; + } + +#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H) + if (hints.ai_family == AF_UNIX) { + const auto addrlen = host.length(); + if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; } + +#ifdef SOCK_CLOEXEC + auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC, + hints.ai_protocol); +#else + auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); +#endif + + if (sock != INVALID_SOCKET) { + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + + auto unescaped_host = unescape_abstract_namespace_unix_domain(host); + std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path); + + hints.ai_addr = reinterpret_cast(&addr); + hints.ai_addrlen = static_cast( + sizeof(addr) - sizeof(addr.sun_path) + addrlen); + +#ifndef SOCK_CLOEXEC +#ifndef _WIN32 + fcntl(sock, F_SETFD, FD_CLOEXEC); +#endif +#endif + + if (socket_options) { socket_options(sock); } + +#ifdef _WIN32 + // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so + // remove the option. + detail::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0); +#endif + + bool dummy; + if (!bind_or_connect(sock, hints, dummy)) { + close_socket(sock); + sock = INVALID_SOCKET; + } + } + return sock; + } +#endif + + auto service = std::to_string(port); + + if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, + timeout_sec)) { +#if defined __linux__ && !defined __ANDROID__ + res_init(); +#endif + return INVALID_SOCKET; + } + auto se = detail::scope_exit([&] { freeaddrinfo(result); }); + + for (auto rp = result; rp; rp = rp->ai_next) { + // Create a socket +#ifdef _WIN32 + auto sock = + WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, + WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); + /** + * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 + * and above the socket creation fails on older Windows Systems. + * + * Let's try to create a socket the old way in this case. + * + * Reference: + * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa + * + * WSA_FLAG_NO_HANDLE_INHERIT: + * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with + * SP1, and later + * + */ + if (sock == INVALID_SOCKET) { + sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + } +#else + +#ifdef SOCK_CLOEXEC + auto sock = + socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol); +#else + auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); +#endif + +#endif + if (sock == INVALID_SOCKET) { continue; } + +#if !defined _WIN32 && !defined SOCK_CLOEXEC + if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { + close_socket(sock); + continue; + } +#endif + + if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); } + + if (rp->ai_family == AF_INET6) { + set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0); + } + + if (socket_options) { socket_options(sock); } + + // bind or connect + auto quit = false; + if (bind_or_connect(sock, *rp, quit)) { return sock; } + + close_socket(sock); + + if (quit) { break; } + } + + return INVALID_SOCKET; +} + +inline void set_nonblocking(socket_t sock, bool nonblocking) { +#ifdef _WIN32 + auto flags = nonblocking ? 1UL : 0UL; + ioctlsocket(sock, FIONBIO, &flags); +#else + auto flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, + nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); +#endif +} + +inline bool is_connection_error() { +#ifdef _WIN32 + return WSAGetLastError() != WSAEWOULDBLOCK; +#else + return errno != EINPROGRESS; +#endif +} + +inline bool bind_ip_address(socket_t sock, const std::string &host) { + struct addrinfo hints; + struct addrinfo *result; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + + if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { + return false; + } + + auto se = detail::scope_exit([&] { freeaddrinfo(result); }); + + auto ret = false; + for (auto rp = result; rp; rp = rp->ai_next) { + const auto &ai = *rp; + if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { + ret = true; + break; + } + } + + return ret; +} + +#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ +#define USE_IF2IP +#endif + +#ifdef USE_IF2IP +inline std::string if2ip(int address_family, const std::string &ifn) { + struct ifaddrs *ifap; + getifaddrs(&ifap); + auto se = detail::scope_exit([&] { freeifaddrs(ifap); }); + + std::string addr_candidate; + for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { + if (ifa->ifa_addr && ifn == ifa->ifa_name && + (AF_UNSPEC == address_family || + ifa->ifa_addr->sa_family == address_family)) { + if (ifa->ifa_addr->sa_family == AF_INET) { + auto sa = reinterpret_cast(ifa->ifa_addr); + char buf[INET_ADDRSTRLEN]; + if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { + return std::string(buf, INET_ADDRSTRLEN); + } + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + auto sa = reinterpret_cast(ifa->ifa_addr); + if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { + char buf[INET6_ADDRSTRLEN] = {}; + if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) { + // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL + auto s6_addr_head = sa->sin6_addr.s6_addr[0]; + if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) { + addr_candidate = std::string(buf, INET6_ADDRSTRLEN); + } else { + return std::string(buf, INET6_ADDRSTRLEN); + } + } + } + } + } + } + return addr_candidate; +} +#endif + +inline socket_t create_client_socket( + const std::string &host, const std::string &ip, int port, + int address_family, bool tcp_nodelay, bool ipv6_v6only, + SocketOptions socket_options, time_t connection_timeout_sec, + time_t connection_timeout_usec, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, const std::string &intf, Error &error) { + auto sock = create_socket( + host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only, + std::move(socket_options), + [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool { + if (!intf.empty()) { +#ifdef USE_IF2IP + auto ip_from_if = if2ip(address_family, intf); + if (ip_from_if.empty()) { ip_from_if = intf; } + if (!bind_ip_address(sock2, ip_from_if)) { + error = Error::BindIPAddress; + return false; + } +#endif + } + + set_nonblocking(sock2, true); + + auto ret = + ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen)); + + if (ret < 0) { + if (is_connection_error()) { + error = Error::Connection; + return false; + } + error = wait_until_socket_is_ready(sock2, connection_timeout_sec, + connection_timeout_usec); + if (error != Error::Success) { + if (error == Error::ConnectionTimeout) { quit = true; } + return false; + } + } + + set_nonblocking(sock2, false); + set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec, + read_timeout_usec); + set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec, + write_timeout_usec); + + error = Error::Success; + return true; + }, + connection_timeout_sec); // Pass DNS timeout + + if (sock != INVALID_SOCKET) { + error = Error::Success; + } else { + if (error == Error::Success) { error = Error::Connection; } + } + + return sock; +} + +inline bool get_ip_and_port(const struct sockaddr_storage &addr, + socklen_t addr_len, std::string &ip, int &port) { + if (addr.ss_family == AF_INET) { + port = ntohs(reinterpret_cast(&addr)->sin_port); + } else if (addr.ss_family == AF_INET6) { + port = + ntohs(reinterpret_cast(&addr)->sin6_port); + } else { + return false; + } + + std::array ipstr{}; + if (getnameinfo(reinterpret_cast(&addr), addr_len, + ipstr.data(), static_cast(ipstr.size()), nullptr, + 0, NI_NUMERICHOST)) { + return false; + } + + ip = ipstr.data(); + return true; +} + +inline void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + if (!getsockname(sock, reinterpret_cast(&addr), + &addr_len)) { + get_ip_and_port(addr, addr_len, ip, port); + } +} + +inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + + if (!getpeername(sock, reinterpret_cast(&addr), + &addr_len)) { +#ifndef _WIN32 + if (addr.ss_family == AF_UNIX) { +#if defined(__linux__) + struct ucred ucred; + socklen_t len = sizeof(ucred); + if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) { + port = ucred.pid; + } +#elif defined(SOL_LOCAL) && defined(SO_PEERPID) + pid_t pid; + socklen_t len = sizeof(pid); + if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) { + port = pid; + } +#endif + return; + } +#endif + get_ip_and_port(addr, addr_len, ip, port); + } +} + +inline constexpr unsigned int str2tag_core(const char *s, size_t l, + unsigned int h) { + return (l == 0) + ? h + : str2tag_core( + s + 1, l - 1, + // Unsets the 6 high bits of h, therefore no overflow happens + (((std::numeric_limits::max)() >> 6) & + h * 33) ^ + static_cast(*s)); +} + +inline unsigned int str2tag(const std::string &s) { + return str2tag_core(s.data(), s.size(), 0); +} + +namespace udl { + +inline constexpr unsigned int operator""_t(const char *s, size_t l) { + return str2tag_core(s, l, 0); +} + +} // namespace udl + +inline std::string +find_content_type(const std::string &path, + const std::map &user_data, + const std::string &default_content_type) { + auto ext = file_extension(path); + + auto it = user_data.find(ext); + if (it != user_data.end()) { return it->second; } + + using udl::operator""_t; + + switch (str2tag(ext)) { + default: return default_content_type; + + case "css"_t: return "text/css"; + case "csv"_t: return "text/csv"; + case "htm"_t: + case "html"_t: return "text/html"; + case "js"_t: + case "mjs"_t: return "text/javascript"; + case "txt"_t: return "text/plain"; + case "vtt"_t: return "text/vtt"; + + case "apng"_t: return "image/apng"; + case "avif"_t: return "image/avif"; + case "bmp"_t: return "image/bmp"; + case "gif"_t: return "image/gif"; + case "png"_t: return "image/png"; + case "svg"_t: return "image/svg+xml"; + case "webp"_t: return "image/webp"; + case "ico"_t: return "image/x-icon"; + case "tif"_t: return "image/tiff"; + case "tiff"_t: return "image/tiff"; + case "jpg"_t: + case "jpeg"_t: return "image/jpeg"; + + case "mp4"_t: return "video/mp4"; + case "mpeg"_t: return "video/mpeg"; + case "webm"_t: return "video/webm"; + + case "mp3"_t: return "audio/mp3"; + case "mpga"_t: return "audio/mpeg"; + case "weba"_t: return "audio/webm"; + case "wav"_t: return "audio/wave"; + + case "otf"_t: return "font/otf"; + case "ttf"_t: return "font/ttf"; + case "woff"_t: return "font/woff"; + case "woff2"_t: return "font/woff2"; + + case "7z"_t: return "application/x-7z-compressed"; + case "atom"_t: return "application/atom+xml"; + case "pdf"_t: return "application/pdf"; + case "json"_t: return "application/json"; + case "rss"_t: return "application/rss+xml"; + case "tar"_t: return "application/x-tar"; + case "xht"_t: + case "xhtml"_t: return "application/xhtml+xml"; + case "xslt"_t: return "application/xslt+xml"; + case "xml"_t: return "application/xml"; + case "gz"_t: return "application/gzip"; + case "zip"_t: return "application/zip"; + case "wasm"_t: return "application/wasm"; + } +} + +inline bool can_compress_content_type(const std::string &content_type) { + using udl::operator""_t; + + auto tag = str2tag(content_type); + + switch (tag) { + case "image/svg+xml"_t: + case "application/javascript"_t: + case "application/json"_t: + case "application/xml"_t: + case "application/protobuf"_t: + case "application/xhtml+xml"_t: return true; + + case "text/event-stream"_t: return false; + + default: return !content_type.rfind("text/", 0); + } +} + +inline EncodingType encoding_type(const Request &req, const Response &res) { + auto ret = + detail::can_compress_content_type(res.get_header_value("Content-Type")); + if (!ret) { return EncodingType::None; } + + const auto &s = req.get_header_value("Accept-Encoding"); + (void)(s); + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + // TODO: 'Accept-Encoding' has br, not br;q=0 + ret = s.find("br") != std::string::npos; + if (ret) { return EncodingType::Brotli; } +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + // TODO: 'Accept-Encoding' has gzip, not gzip;q=0 + ret = s.find("gzip") != std::string::npos; + if (ret) { return EncodingType::Gzip; } +#endif + +#ifdef CPPHTTPLIB_ZSTD_SUPPORT + // TODO: 'Accept-Encoding' has zstd, not zstd;q=0 + ret = s.find("zstd") != std::string::npos; + if (ret) { return EncodingType::Zstd; } +#endif + + return EncodingType::None; +} + +inline bool nocompressor::compress(const char *data, size_t data_length, + bool /*last*/, Callback callback) { + if (!data_length) { return true; } + return callback(data, data_length); +} + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +inline gzip_compressor::gzip_compressor() { + std::memset(&strm_, 0, sizeof(strm_)); + strm_.zalloc = Z_NULL; + strm_.zfree = Z_NULL; + strm_.opaque = Z_NULL; + + is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, + Z_DEFAULT_STRATEGY) == Z_OK; +} + +inline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } + +inline bool gzip_compressor::compress(const char *data, size_t data_length, + bool last, Callback callback) { + assert(is_valid_); + + do { + constexpr size_t max_avail_in = + (std::numeric_limits::max)(); + + strm_.avail_in = static_cast( + (std::min)(data_length, max_avail_in)); + strm_.next_in = const_cast(reinterpret_cast(data)); + + data_length -= strm_.avail_in; + data += strm_.avail_in; + + auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; + auto ret = Z_OK; + + std::array buff{}; + do { + strm_.avail_out = static_cast(buff.size()); + strm_.next_out = reinterpret_cast(buff.data()); + + ret = deflate(&strm_, flush); + if (ret == Z_STREAM_ERROR) { return false; } + + if (!callback(buff.data(), buff.size() - strm_.avail_out)) { + return false; + } + } while (strm_.avail_out == 0); + + assert((flush == Z_FINISH && ret == Z_STREAM_END) || + (flush == Z_NO_FLUSH && ret == Z_OK)); + assert(strm_.avail_in == 0); + } while (data_length > 0); + + return true; +} + +inline gzip_decompressor::gzip_decompressor() { + std::memset(&strm_, 0, sizeof(strm_)); + strm_.zalloc = Z_NULL; + strm_.zfree = Z_NULL; + strm_.opaque = Z_NULL; + + // 15 is the value of wbits, which should be at the maximum possible value + // to ensure that any gzip stream can be decoded. The offset of 32 specifies + // that the stream type should be automatically detected either gzip or + // deflate. + is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK; +} + +inline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } + +inline bool gzip_decompressor::is_valid() const { return is_valid_; } + +inline bool gzip_decompressor::decompress(const char *data, size_t data_length, + Callback callback) { + assert(is_valid_); + + auto ret = Z_OK; + + do { + constexpr size_t max_avail_in = + (std::numeric_limits::max)(); + + strm_.avail_in = static_cast( + (std::min)(data_length, max_avail_in)); + strm_.next_in = const_cast(reinterpret_cast(data)); + + data_length -= strm_.avail_in; + data += strm_.avail_in; + + std::array buff{}; + while (strm_.avail_in > 0 && ret == Z_OK) { + strm_.avail_out = static_cast(buff.size()); + strm_.next_out = reinterpret_cast(buff.data()); + + ret = inflate(&strm_, Z_NO_FLUSH); + + assert(ret != Z_STREAM_ERROR); + switch (ret) { + case Z_NEED_DICT: + case Z_DATA_ERROR: + case Z_MEM_ERROR: inflateEnd(&strm_); return false; + } + + if (!callback(buff.data(), buff.size() - strm_.avail_out)) { + return false; + } + } + + if (ret != Z_OK && ret != Z_STREAM_END) { return false; } + + } while (data_length > 0); + + return true; +} +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +inline brotli_compressor::brotli_compressor() { + state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); +} + +inline brotli_compressor::~brotli_compressor() { + BrotliEncoderDestroyInstance(state_); +} + +inline bool brotli_compressor::compress(const char *data, size_t data_length, + bool last, Callback callback) { + std::array buff{}; + + auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; + auto available_in = data_length; + auto next_in = reinterpret_cast(data); + + for (;;) { + if (last) { + if (BrotliEncoderIsFinished(state_)) { break; } + } else { + if (!available_in) { break; } + } + + auto available_out = buff.size(); + auto next_out = buff.data(); + + if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in, + &available_out, &next_out, nullptr)) { + return false; + } + + auto output_bytes = buff.size() - available_out; + if (output_bytes) { + callback(reinterpret_cast(buff.data()), output_bytes); + } + } + + return true; +} + +inline brotli_decompressor::brotli_decompressor() { + decoder_s = BrotliDecoderCreateInstance(0, 0, 0); + decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT + : BROTLI_DECODER_RESULT_ERROR; +} + +inline brotli_decompressor::~brotli_decompressor() { + if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } +} + +inline bool brotli_decompressor::is_valid() const { return decoder_s; } + +inline bool brotli_decompressor::decompress(const char *data, + size_t data_length, + Callback callback) { + if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS || + decoder_r == BROTLI_DECODER_RESULT_ERROR) { + return 0; + } + + auto next_in = reinterpret_cast(data); + size_t avail_in = data_length; + size_t total_out; + + decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; + + std::array buff{}; + while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { + char *next_out = buff.data(); + size_t avail_out = buff.size(); + + decoder_r = BrotliDecoderDecompressStream( + decoder_s, &avail_in, &next_in, &avail_out, + reinterpret_cast(&next_out), &total_out); + + if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; } + + if (!callback(buff.data(), buff.size() - avail_out)) { return false; } + } + + return decoder_r == BROTLI_DECODER_RESULT_SUCCESS || + decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; +} +#endif + +#ifdef CPPHTTPLIB_ZSTD_SUPPORT +inline zstd_compressor::zstd_compressor() { + ctx_ = ZSTD_createCCtx(); + ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast); +} + +inline zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); } + +inline bool zstd_compressor::compress(const char *data, size_t data_length, + bool last, Callback callback) { + std::array buff{}; + + ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue; + ZSTD_inBuffer input = {data, data_length, 0}; + + bool finished; + do { + ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; + size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode); + + if (ZSTD_isError(remaining)) { return false; } + + if (!callback(buff.data(), output.pos)) { return false; } + + finished = last ? (remaining == 0) : (input.pos == input.size); + + } while (!finished); + + return true; +} + +inline zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); } + +inline zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); } + +inline bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; } + +inline bool zstd_decompressor::decompress(const char *data, size_t data_length, + Callback callback) { + std::array buff{}; + ZSTD_inBuffer input = {data, data_length, 0}; + + while (input.pos < input.size) { + ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; + size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input); + + if (ZSTD_isError(remaining)) { return false; } + + if (!callback(buff.data(), output.pos)) { return false; } + } + + return true; +} +#endif + +inline std::unique_ptr +create_decompressor(const std::string &encoding) { + std::unique_ptr decompressor; + + if (encoding == "gzip" || encoding == "deflate") { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + decompressor = detail::make_unique(); +#endif + } else if (encoding.find("br") != std::string::npos) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + decompressor = detail::make_unique(); +#endif + } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) { +#ifdef CPPHTTPLIB_ZSTD_SUPPORT + decompressor = detail::make_unique(); +#endif + } + + return decompressor; +} + +inline bool is_prohibited_header_name(const std::string &name) { + using udl::operator""_t; + + switch (str2tag(name)) { + case "REMOTE_ADDR"_t: + case "REMOTE_PORT"_t: + case "LOCAL_ADDR"_t: + case "LOCAL_PORT"_t: return true; + default: return false; + } +} + +inline bool has_header(const Headers &headers, const std::string &key) { + if (is_prohibited_header_name(key)) { return false; } + return headers.find(key) != headers.end(); +} + +inline const char *get_header_value(const Headers &headers, + const std::string &key, const char *def, + size_t id) { + if (is_prohibited_header_name(key)) { +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + std::string msg = "Prohibited header name '" + key + "' is specified."; + throw std::invalid_argument(msg); +#else + return ""; +#endif + } + + auto rng = headers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second.c_str(); } + return def; +} + +inline bool read_headers(Stream &strm, Headers &headers) { + const auto bufsiz = 2048; + char buf[bufsiz]; + stream_line_reader line_reader(strm, buf, bufsiz); + + size_t header_count = 0; + + for (;;) { + if (!line_reader.getline()) { return false; } + + // Check if the line ends with CRLF. + auto line_terminator_len = 2; + if (line_reader.end_with_crlf()) { + // Blank line indicates end of headers. + if (line_reader.size() == 2) { break; } + } else { +#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + // Blank line indicates end of headers. + if (line_reader.size() == 1) { break; } + line_terminator_len = 1; +#else + continue; // Skip invalid line. +#endif + } + + if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + + // Check header count limit + if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } + + // Exclude line terminator + auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; + + if (!parse_header(line_reader.ptr(), end, + [&](const std::string &key, const std::string &val) { + headers.emplace(key, val); + })) { + return false; + } + + header_count++; + } + + return true; +} + +inline bool read_content_with_length(Stream &strm, size_t len, + DownloadProgress progress, + ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + + detail::BodyReader br; + br.stream = &strm; + br.content_length = len; + br.chunked = false; + br.bytes_read = 0; + br.last_error = Error::Success; + + size_t r = 0; + while (r < len) { + auto read_len = static_cast(len - r); + auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ); + auto n = detail::read_body_content(&strm, br, buf, to_read); + if (n <= 0) { return false; } + + if (!out(buf, static_cast(n), r, len)) { return false; } + r += static_cast(n); + + if (progress) { + if (!progress(r, len)) { return false; } + } + } + + return true; +} + +inline void skip_content_with_length(Stream &strm, size_t len) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + size_t r = 0; + while (r < len) { + auto read_len = static_cast(len - r); + auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); + if (n <= 0) { return; } + r += static_cast(n); + } +} + +enum class ReadContentResult { + Success, // Successfully read the content + PayloadTooLarge, // The content exceeds the specified payload limit + Error // An error occurred while reading the content +}; + +inline ReadContentResult +read_content_without_length(Stream &strm, size_t payload_max_length, + ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + size_t r = 0; + for (;;) { + auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); + if (n == 0) { return ReadContentResult::Success; } + if (n < 0) { return ReadContentResult::Error; } + + // Check if adding this data would exceed the payload limit + if (r > payload_max_length || + payload_max_length - r < static_cast(n)) { + return ReadContentResult::PayloadTooLarge; + } + + if (!out(buf, static_cast(n), r, 0)) { + return ReadContentResult::Error; + } + r += static_cast(n); + } + + return ReadContentResult::Success; +} + +template +inline ReadContentResult read_content_chunked(Stream &strm, T &x, + size_t payload_max_length, + ContentReceiverWithProgress out) { + detail::ChunkedDecoder dec(strm); + + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + size_t total_len = 0; + + for (;;) { + size_t chunk_offset = 0; + size_t chunk_total = 0; + auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total); + if (n < 0) { return ReadContentResult::Error; } + + if (n == 0) { + if (!dec.parse_trailers_into(x.trailers, x.headers)) { + return ReadContentResult::Error; + } + return ReadContentResult::Success; + } + + if (total_len > payload_max_length || + payload_max_length - total_len < static_cast(n)) { + return ReadContentResult::PayloadTooLarge; + } + + if (!out(buf, static_cast(n), chunk_offset, chunk_total)) { + return ReadContentResult::Error; + } + + total_len += static_cast(n); + } +} + +inline bool is_chunked_transfer_encoding(const Headers &headers) { + return case_ignore::equal( + get_header_value(headers, "Transfer-Encoding", "", 0), "chunked"); +} + +template +bool prepare_content_receiver(T &x, int &status, + ContentReceiverWithProgress receiver, + bool decompress, U callback) { + if (decompress) { + std::string encoding = x.get_header_value("Content-Encoding"); + std::unique_ptr decompressor; + + if (!encoding.empty()) { + decompressor = detail::create_decompressor(encoding); + if (!decompressor) { + // Unsupported encoding or no support compiled in + status = StatusCode::UnsupportedMediaType_415; + return false; + } + } + + if (decompressor) { + if (decompressor->is_valid()) { + ContentReceiverWithProgress out = [&](const char *buf, size_t n, + size_t off, size_t len) { + return decompressor->decompress(buf, n, + [&](const char *buf2, size_t n2) { + return receiver(buf2, n2, off, len); + }); + }; + return callback(std::move(out)); + } else { + status = StatusCode::InternalServerError_500; + return false; + } + } + } + + ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off, + size_t len) { + return receiver(buf, n, off, len); + }; + return callback(std::move(out)); +} + +template +bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, + DownloadProgress progress, + ContentReceiverWithProgress receiver, bool decompress) { + return prepare_content_receiver( + x, status, std::move(receiver), decompress, + [&](const ContentReceiverWithProgress &out) { + auto ret = true; + auto exceed_payload_max_length = false; + + if (is_chunked_transfer_encoding(x.headers)) { + auto result = read_content_chunked(strm, x, payload_max_length, out); + if (result == ReadContentResult::Success) { + ret = true; + } else if (result == ReadContentResult::PayloadTooLarge) { + exceed_payload_max_length = true; + ret = false; + } else { + ret = false; + } + } else if (!has_header(x.headers, "Content-Length")) { + auto result = + read_content_without_length(strm, payload_max_length, out); + if (result == ReadContentResult::Success) { + ret = true; + } else if (result == ReadContentResult::PayloadTooLarge) { + exceed_payload_max_length = true; + ret = false; + } else { + ret = false; + } + } else { + auto is_invalid_value = false; + auto len = get_header_value_u64(x.headers, "Content-Length", + (std::numeric_limits::max)(), + 0, is_invalid_value); + + if (is_invalid_value) { + ret = false; + } else if (len > payload_max_length) { + exceed_payload_max_length = true; + skip_content_with_length(strm, len); + ret = false; + } else if (len > 0) { + ret = read_content_with_length(strm, len, std::move(progress), out); + } + } + + if (!ret) { + status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413 + : StatusCode::BadRequest_400; + } + return ret; + }); +} + +inline ssize_t write_request_line(Stream &strm, const std::string &method, + const std::string &path) { + std::string s = method; + s += " "; + s += path; + s += " HTTP/1.1\r\n"; + return strm.write(s.data(), s.size()); +} + +inline ssize_t write_response_line(Stream &strm, int status) { + std::string s = "HTTP/1.1 "; + s += std::to_string(status); + s += " "; + s += httplib::status_message(status); + s += "\r\n"; + return strm.write(s.data(), s.size()); +} + +inline ssize_t write_headers(Stream &strm, const Headers &headers) { + ssize_t write_len = 0; + for (const auto &x : headers) { + std::string s; + s = x.first; + s += ": "; + s += x.second; + s += "\r\n"; + + auto len = strm.write(s.data(), s.size()); + if (len < 0) { return len; } + write_len += len; + } + auto len = strm.write("\r\n"); + if (len < 0) { return len; } + write_len += len; + return write_len; +} + +inline bool write_data(Stream &strm, const char *d, size_t l) { + size_t offset = 0; + while (offset < l) { + auto length = strm.write(d + offset, l - offset); + if (length < 0) { return false; } + offset += static_cast(length); + } + return true; +} + +template +inline bool write_content_with_progress(Stream &strm, + const ContentProvider &content_provider, + size_t offset, size_t length, + T is_shutting_down, + const UploadProgress &upload_progress, + Error &error) { + size_t end_offset = offset + length; + size_t start_offset = offset; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + if (write_data(strm, d, l)) { + offset += l; + + if (upload_progress && length > 0) { + size_t current_written = offset - start_offset; + if (!upload_progress(current_written, length)) { + ok = false; + return false; + } + } + } else { + ok = false; + } + } + return ok; + }; + + data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); }; + + while (offset < end_offset && !is_shutting_down()) { + if (!strm.wait_writable()) { + error = Error::Write; + return false; + } else if (!content_provider(offset, end_offset - offset, data_sink)) { + error = Error::Canceled; + return false; + } else if (!ok) { + error = Error::Write; + return false; + } + } + + error = Error::Success; + return true; +} + +template +inline bool write_content(Stream &strm, const ContentProvider &content_provider, + size_t offset, size_t length, T is_shutting_down, + Error &error) { + return write_content_with_progress(strm, content_provider, offset, length, + is_shutting_down, nullptr, error); +} + +template +inline bool write_content(Stream &strm, const ContentProvider &content_provider, + size_t offset, size_t length, + const T &is_shutting_down) { + auto error = Error::Success; + return write_content(strm, content_provider, offset, length, is_shutting_down, + error); +} + +template +inline bool +write_content_without_length(Stream &strm, + const ContentProvider &content_provider, + const T &is_shutting_down) { + size_t offset = 0; + auto data_available = true; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + offset += l; + if (!write_data(strm, d, l)) { ok = false; } + } + return ok; + }; + + data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); }; + + data_sink.done = [&](void) { data_available = false; }; + + while (data_available && !is_shutting_down()) { + if (!strm.wait_writable()) { + return false; + } else if (!content_provider(offset, 0, data_sink)) { + return false; + } else if (!ok) { + return false; + } + } + return true; +} + +template +inline bool +write_content_chunked(Stream &strm, const ContentProvider &content_provider, + const T &is_shutting_down, U &compressor, Error &error) { + size_t offset = 0; + auto data_available = true; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + data_available = l > 0; + offset += l; + + std::string payload; + if (compressor.compress(d, l, false, + [&](const char *data, size_t data_len) { + payload.append(data, data_len); + return true; + })) { + if (!payload.empty()) { + // Emit chunked response header and footer for each chunk + auto chunk = + from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; + if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; } + } + } else { + ok = false; + } + } + return ok; + }; + + data_sink.is_writable = [&]() -> bool { return strm.wait_writable(); }; + + auto done_with_trailer = [&](const Headers *trailer) { + if (!ok) { return; } + + data_available = false; + + std::string payload; + if (!compressor.compress(nullptr, 0, true, + [&](const char *data, size_t data_len) { + payload.append(data, data_len); + return true; + })) { + ok = false; + return; + } + + if (!payload.empty()) { + // Emit chunked response header and footer for each chunk + auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; + if (!write_data(strm, chunk.data(), chunk.size())) { + ok = false; + return; + } + } + + constexpr const char done_marker[] = "0\r\n"; + if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; } + + // Trailer + if (trailer) { + for (const auto &kv : *trailer) { + std::string field_line = kv.first + ": " + kv.second + "\r\n"; + if (!write_data(strm, field_line.data(), field_line.size())) { + ok = false; + } + } + } + + constexpr const char crlf[] = "\r\n"; + if (!write_data(strm, crlf, str_len(crlf))) { ok = false; } + }; + + data_sink.done = [&](void) { done_with_trailer(nullptr); }; + + data_sink.done_with_trailer = [&](const Headers &trailer) { + done_with_trailer(&trailer); + }; + + while (data_available && !is_shutting_down()) { + if (!strm.wait_writable()) { + error = Error::Write; + return false; + } else if (!content_provider(offset, 0, data_sink)) { + error = Error::Canceled; + return false; + } else if (!ok) { + error = Error::Write; + return false; + } + } + + error = Error::Success; + return true; +} + +template +inline bool write_content_chunked(Stream &strm, + const ContentProvider &content_provider, + const T &is_shutting_down, U &compressor) { + auto error = Error::Success; + return write_content_chunked(strm, content_provider, is_shutting_down, + compressor, error); +} + +template +inline bool redirect(T &cli, Request &req, Response &res, + const std::string &path, const std::string &location, + Error &error) { + Request new_req = req; + new_req.path = path; + new_req.redirect_count_ -= 1; + + if (res.status == StatusCode::SeeOther_303 && + (req.method != "GET" && req.method != "HEAD")) { + new_req.method = "GET"; + new_req.body.clear(); + new_req.headers.clear(); + } + + Response new_res; + + auto ret = cli.send(new_req, new_res, error); + if (ret) { + req = new_req; + res = new_res; + + if (res.location.empty()) { res.location = location; } + } + return ret; +} + +inline std::string params_to_query_str(const Params ¶ms) { + std::string query; + + for (auto it = params.begin(); it != params.end(); ++it) { + if (it != params.begin()) { query += "&"; } + query += encode_query_component(it->first); + query += "="; + query += encode_query_component(it->second); + } + return query; +} + +inline void parse_query_text(const char *data, std::size_t size, + Params ¶ms) { + std::set cache; + split(data, data + size, '&', [&](const char *b, const char *e) { + std::string kv(b, e); + if (cache.find(kv) != cache.end()) { return; } + cache.insert(std::move(kv)); + + std::string key; + std::string val; + divide(b, static_cast(e - b), '=', + [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, + std::size_t rhs_size) { + key.assign(lhs_data, lhs_size); + val.assign(rhs_data, rhs_size); + }); + + if (!key.empty()) { + params.emplace(decode_query_component(key), decode_query_component(val)); + } + }); +} + +inline void parse_query_text(const std::string &s, Params ¶ms) { + parse_query_text(s.data(), s.size(), params); +} + +inline bool parse_multipart_boundary(const std::string &content_type, + std::string &boundary) { + auto boundary_keyword = "boundary="; + auto pos = content_type.find(boundary_keyword); + if (pos == std::string::npos) { return false; } + auto end = content_type.find(';', pos); + auto beg = pos + strlen(boundary_keyword); + boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg)); + return !boundary.empty(); +} + +inline void parse_disposition_params(const std::string &s, Params ¶ms) { + std::set cache; + split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) { + std::string kv(b, e); + if (cache.find(kv) != cache.end()) { return; } + cache.insert(kv); + + std::string key; + std::string val; + split(b, e, '=', [&](const char *b2, const char *e2) { + if (key.empty()) { + key.assign(b2, e2); + } else { + val.assign(b2, e2); + } + }); + + if (!key.empty()) { + params.emplace(trim_double_quotes_copy((key)), + trim_double_quotes_copy((val))); + } + }); +} + +#ifdef CPPHTTPLIB_NO_EXCEPTIONS +inline bool parse_range_header(const std::string &s, Ranges &ranges) { +#else +inline bool parse_range_header(const std::string &s, Ranges &ranges) try { +#endif + auto is_valid = [](const std::string &str) { + return std::all_of(str.cbegin(), str.cend(), + [](unsigned char c) { return std::isdigit(c); }); + }; + + if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) { + const auto pos = static_cast(6); + const auto len = static_cast(s.size() - 6); + auto all_valid_ranges = true; + split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { + if (!all_valid_ranges) { return; } + + const auto it = std::find(b, e, '-'); + if (it == e) { + all_valid_ranges = false; + return; + } + + const auto lhs = std::string(b, it); + const auto rhs = std::string(it + 1, e); + if (!is_valid(lhs) || !is_valid(rhs)) { + all_valid_ranges = false; + return; + } + + const auto first = + static_cast(lhs.empty() ? -1 : std::stoll(lhs)); + const auto last = + static_cast(rhs.empty() ? -1 : std::stoll(rhs)); + if ((first == -1 && last == -1) || + (first != -1 && last != -1 && first > last)) { + all_valid_ranges = false; + return; + } + + ranges.emplace_back(first, last); + }); + return all_valid_ranges && !ranges.empty(); + } + return false; +#ifdef CPPHTTPLIB_NO_EXCEPTIONS +} +#else +} catch (...) { return false; } +#endif + +inline bool parse_accept_header(const std::string &s, + std::vector &content_types) { + content_types.clear(); + + // Empty string is considered valid (no preference) + if (s.empty()) { return true; } + + // Check for invalid patterns: leading/trailing commas or consecutive commas + if (s.front() == ',' || s.back() == ',' || + s.find(",,") != std::string::npos) { + return false; + } + + struct AcceptEntry { + std::string media_type; + double quality; + int order; // Original order in header + }; + + std::vector entries; + int order = 0; + bool has_invalid_entry = false; + + // Split by comma and parse each entry + split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { + std::string entry(b, e); + entry = trim_copy(entry); + + if (entry.empty()) { + has_invalid_entry = true; + return; + } + + AcceptEntry accept_entry; + accept_entry.quality = 1.0; // Default quality + accept_entry.order = order++; + + // Find q= parameter + auto q_pos = entry.find(";q="); + if (q_pos == std::string::npos) { q_pos = entry.find("; q="); } + + if (q_pos != std::string::npos) { + // Extract media type (before q parameter) + accept_entry.media_type = trim_copy(entry.substr(0, q_pos)); + + // Extract quality value + auto q_start = entry.find('=', q_pos) + 1; + auto q_end = entry.find(';', q_start); + if (q_end == std::string::npos) { q_end = entry.length(); } + + std::string quality_str = + trim_copy(entry.substr(q_start, q_end - q_start)); + if (quality_str.empty()) { + has_invalid_entry = true; + return; + } + +#ifdef CPPHTTPLIB_NO_EXCEPTIONS + { + std::istringstream iss(quality_str); + iss >> accept_entry.quality; + + // Check if conversion was successful and entire string was consumed + if (iss.fail() || !iss.eof()) { + has_invalid_entry = true; + return; + } + } +#else + try { + accept_entry.quality = std::stod(quality_str); + } catch (...) { + has_invalid_entry = true; + return; + } +#endif + // Check if quality is in valid range [0.0, 1.0] + if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) { + has_invalid_entry = true; + return; + } + } else { + // No quality parameter, use entire entry as media type + accept_entry.media_type = entry; + } + + // Remove additional parameters from media type + auto param_pos = accept_entry.media_type.find(';'); + if (param_pos != std::string::npos) { + accept_entry.media_type = + trim_copy(accept_entry.media_type.substr(0, param_pos)); + } + + // Basic validation of media type format + if (accept_entry.media_type.empty()) { + has_invalid_entry = true; + return; + } + + // Check for basic media type format (should contain '/' or be '*') + if (accept_entry.media_type != "*" && + accept_entry.media_type.find('/') == std::string::npos) { + has_invalid_entry = true; + return; + } + + entries.push_back(accept_entry); + }); + + // Return false if any invalid entry was found + if (has_invalid_entry) { return false; } + + // Sort by quality (descending), then by original order (ascending) + std::sort(entries.begin(), entries.end(), + [](const AcceptEntry &a, const AcceptEntry &b) { + if (a.quality != b.quality) { + return a.quality > b.quality; // Higher quality first + } + return a.order < b.order; // Earlier order first for same quality + }); + + // Extract sorted media types + content_types.reserve(entries.size()); + for (const auto &entry : entries) { + content_types.push_back(entry.media_type); + } + + return true; +} + +class FormDataParser { +public: + FormDataParser() = default; + + void set_boundary(std::string &&boundary) { + boundary_ = boundary; + dash_boundary_crlf_ = dash_ + boundary_ + crlf_; + crlf_dash_boundary_ = crlf_ + dash_ + boundary_; + } + + bool is_valid() const { return is_valid_; } + + bool parse(const char *buf, size_t n, const FormDataHeader &header_callback, + const ContentReceiver &content_callback) { + + buf_append(buf, n); + + while (buf_size() > 0) { + switch (state_) { + case 0: { // Initial boundary + auto pos = buf_find(dash_boundary_crlf_); + if (pos == buf_size()) { return true; } + buf_erase(pos + dash_boundary_crlf_.size()); + state_ = 1; + break; + } + case 1: { // New entry + clear_file_info(); + state_ = 2; + break; + } + case 2: { // Headers + auto pos = buf_find(crlf_); + if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + while (pos < buf_size()) { + // Empty line + if (pos == 0) { + if (!header_callback(file_)) { + is_valid_ = false; + return false; + } + buf_erase(crlf_.size()); + state_ = 3; + break; + } + + const auto header = buf_head(pos); + + if (!parse_header(header.data(), header.data() + header.size(), + [&](const std::string &, const std::string &) {})) { + is_valid_ = false; + return false; + } + + // Parse and emplace space trimmed headers into a map + if (!parse_header( + header.data(), header.data() + header.size(), + [&](const std::string &key, const std::string &val) { + file_.headers.emplace(key, val); + })) { + is_valid_ = false; + return false; + } + + constexpr const char header_content_type[] = "Content-Type:"; + + if (start_with_case_ignore(header, header_content_type)) { + file_.content_type = + trim_copy(header.substr(str_len(header_content_type))); + } else { + thread_local const std::regex re_content_disposition( + R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~", + std::regex_constants::icase); + + std::smatch m; + if (std::regex_match(header, m, re_content_disposition)) { + Params params; + parse_disposition_params(m[1], params); + + auto it = params.find("name"); + if (it != params.end()) { + file_.name = it->second; + } else { + is_valid_ = false; + return false; + } + + it = params.find("filename"); + if (it != params.end()) { file_.filename = it->second; } + + it = params.find("filename*"); + if (it != params.end()) { + // Only allow UTF-8 encoding... + thread_local const std::regex re_rfc5987_encoding( + R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase); + + std::smatch m2; + if (std::regex_match(it->second, m2, re_rfc5987_encoding)) { + file_.filename = decode_path_component(m2[1]); // override... + } else { + is_valid_ = false; + return false; + } + } + } + } + buf_erase(pos + crlf_.size()); + pos = buf_find(crlf_); + } + if (state_ != 3) { return true; } + break; + } + case 3: { // Body + if (crlf_dash_boundary_.size() > buf_size()) { return true; } + auto pos = buf_find(crlf_dash_boundary_); + if (pos < buf_size()) { + if (!content_callback(buf_data(), pos)) { + is_valid_ = false; + return false; + } + buf_erase(pos + crlf_dash_boundary_.size()); + state_ = 4; + } else { + auto len = buf_size() - crlf_dash_boundary_.size(); + if (len > 0) { + if (!content_callback(buf_data(), len)) { + is_valid_ = false; + return false; + } + buf_erase(len); + } + return true; + } + break; + } + case 4: { // Boundary + if (crlf_.size() > buf_size()) { return true; } + if (buf_start_with(crlf_)) { + buf_erase(crlf_.size()); + state_ = 1; + } else { + if (dash_.size() > buf_size()) { return true; } + if (buf_start_with(dash_)) { + buf_erase(dash_.size()); + is_valid_ = true; + buf_erase(buf_size()); // Remove epilogue + } else { + return true; + } + } + break; + } + } + } + + return true; + } + +private: + void clear_file_info() { + file_.name.clear(); + file_.filename.clear(); + file_.content_type.clear(); + file_.headers.clear(); + } + + bool start_with_case_ignore(const std::string &a, const char *b) const { + const auto b_len = strlen(b); + if (a.size() < b_len) { return false; } + for (size_t i = 0; i < b_len; i++) { + if (case_ignore::to_lower(a[i]) != case_ignore::to_lower(b[i])) { + return false; + } + } + return true; + } + + const std::string dash_ = "--"; + const std::string crlf_ = "\r\n"; + std::string boundary_; + std::string dash_boundary_crlf_; + std::string crlf_dash_boundary_; + + size_t state_ = 0; + bool is_valid_ = false; + FormData file_; + + // Buffer + bool start_with(const std::string &a, size_t spos, size_t epos, + const std::string &b) const { + if (epos - spos < b.size()) { return false; } + for (size_t i = 0; i < b.size(); i++) { + if (a[i + spos] != b[i]) { return false; } + } + return true; + } + + size_t buf_size() const { return buf_epos_ - buf_spos_; } + + const char *buf_data() const { return &buf_[buf_spos_]; } + + std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); } + + bool buf_start_with(const std::string &s) const { + return start_with(buf_, buf_spos_, buf_epos_, s); + } + + size_t buf_find(const std::string &s) const { + auto c = s.front(); + + size_t off = buf_spos_; + while (off < buf_epos_) { + auto pos = off; + while (true) { + if (pos == buf_epos_) { return buf_size(); } + if (buf_[pos] == c) { break; } + pos++; + } + + auto remaining_size = buf_epos_ - pos; + if (s.size() > remaining_size) { return buf_size(); } + + if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; } + + off = pos + 1; + } + + return buf_size(); + } + + void buf_append(const char *data, size_t n) { + auto remaining_size = buf_size(); + if (remaining_size > 0 && buf_spos_ > 0) { + for (size_t i = 0; i < remaining_size; i++) { + buf_[i] = buf_[buf_spos_ + i]; + } + } + buf_spos_ = 0; + buf_epos_ = remaining_size; + + if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); } + + for (size_t i = 0; i < n; i++) { + buf_[buf_epos_ + i] = data[i]; + } + buf_epos_ += n; + } + + void buf_erase(size_t size) { buf_spos_ += size; } + + std::string buf_; + size_t buf_spos_ = 0; + size_t buf_epos_ = 0; +}; + +inline std::string random_string(size_t length) { + constexpr const char data[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + thread_local auto engine([]() { + // std::random_device might actually be deterministic on some + // platforms, but due to lack of support in the c++ standard library, + // doing better requires either some ugly hacks or breaking portability. + std::random_device seed_gen; + // Request 128 bits of entropy for initialization + std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; + return std::mt19937(seed_sequence); + }()); + + std::string result; + for (size_t i = 0; i < length; i++) { + result += data[engine() % (sizeof(data) - 1)]; + } + return result; +} + +inline std::string make_multipart_data_boundary() { + return "--cpp-httplib-multipart-data-" + detail::random_string(16); +} + +inline bool is_multipart_boundary_chars_valid(const std::string &boundary) { + auto valid = true; + for (size_t i = 0; i < boundary.size(); i++) { + auto c = boundary[i]; + if (!std::isalnum(c) && c != '-' && c != '_') { + valid = false; + break; + } + } + return valid; +} + +template +inline std::string +serialize_multipart_formdata_item_begin(const T &item, + const std::string &boundary) { + std::string body = "--" + boundary + "\r\n"; + body += "Content-Disposition: form-data; name=\"" + item.name + "\""; + if (!item.filename.empty()) { + body += "; filename=\"" + item.filename + "\""; + } + body += "\r\n"; + if (!item.content_type.empty()) { + body += "Content-Type: " + item.content_type + "\r\n"; + } + body += "\r\n"; + + return body; +} + +inline std::string serialize_multipart_formdata_item_end() { return "\r\n"; } + +inline std::string +serialize_multipart_formdata_finish(const std::string &boundary) { + return "--" + boundary + "--\r\n"; +} + +inline std::string +serialize_multipart_formdata_get_content_type(const std::string &boundary) { + return "multipart/form-data; boundary=" + boundary; +} + +inline std::string +serialize_multipart_formdata(const UploadFormDataItems &items, + const std::string &boundary, bool finish = true) { + std::string body; + + for (const auto &item : items) { + body += serialize_multipart_formdata_item_begin(item, boundary); + body += item.content + serialize_multipart_formdata_item_end(); + } + + if (finish) { body += serialize_multipart_formdata_finish(boundary); } + + return body; +} + +inline void coalesce_ranges(Ranges &ranges, size_t content_length) { + if (ranges.size() <= 1) return; + + // Sort ranges by start position + std::sort(ranges.begin(), ranges.end(), + [](const Range &a, const Range &b) { return a.first < b.first; }); + + Ranges coalesced; + coalesced.reserve(ranges.size()); + + for (auto &r : ranges) { + auto first_pos = r.first; + auto last_pos = r.second; + + // Handle special cases like in range_error + if (first_pos == -1 && last_pos == -1) { + first_pos = 0; + last_pos = static_cast(content_length); + } + + if (first_pos == -1) { + first_pos = static_cast(content_length) - last_pos; + last_pos = static_cast(content_length) - 1; + } + + if (last_pos == -1 || last_pos >= static_cast(content_length)) { + last_pos = static_cast(content_length) - 1; + } + + // Skip invalid ranges + if (!(0 <= first_pos && first_pos <= last_pos && + last_pos < static_cast(content_length))) { + continue; + } + + // Coalesce with previous range if overlapping or adjacent (but not + // identical) + if (!coalesced.empty()) { + auto &prev = coalesced.back(); + // Check if current range overlaps or is adjacent to previous range + // but don't coalesce identical ranges (allow duplicates) + if (first_pos <= prev.second + 1 && + !(first_pos == prev.first && last_pos == prev.second)) { + // Extend the previous range + prev.second = (std::max)(prev.second, last_pos); + continue; + } + } + + // Add new range + coalesced.emplace_back(first_pos, last_pos); + } + + ranges = std::move(coalesced); +} + +inline bool range_error(Request &req, Response &res) { + if (!req.ranges.empty() && 200 <= res.status && res.status < 300) { + ssize_t content_len = static_cast( + res.content_length_ ? res.content_length_ : res.body.size()); + + std::vector> processed_ranges; + size_t overwrapping_count = 0; + + // NOTE: The following Range check is based on '14.2. Range' in RFC 9110 + // 'HTTP Semantics' to avoid potential denial-of-service attacks. + // https://www.rfc-editor.org/rfc/rfc9110#section-14.2 + + // Too many ranges + if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; } + + for (auto &r : req.ranges) { + auto &first_pos = r.first; + auto &last_pos = r.second; + + if (first_pos == -1 && last_pos == -1) { + first_pos = 0; + last_pos = content_len; + } + + if (first_pos == -1) { + first_pos = content_len - last_pos; + last_pos = content_len - 1; + } + + // NOTE: RFC-9110 '14.1.2. Byte Ranges': + // A client can limit the number of bytes requested without knowing the + // size of the selected representation. If the last-pos value is absent, + // or if the value is greater than or equal to the current length of the + // representation data, the byte range is interpreted as the remainder of + // the representation (i.e., the server replaces the value of last-pos + // with a value that is one less than the current length of the selected + // representation). + // https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6 + if (last_pos == -1 || last_pos >= content_len) { + last_pos = content_len - 1; + } + + // Range must be within content length + if (!(0 <= first_pos && first_pos <= last_pos && + last_pos <= content_len - 1)) { + return true; + } + + // Request must not have more than two overlapping ranges + for (const auto &processed_range : processed_ranges) { + if (!(last_pos < processed_range.first || + first_pos > processed_range.second)) { + overwrapping_count++; + if (overwrapping_count > 2) { return true; } + break; // Only count once per range + } + } + + processed_ranges.emplace_back(first_pos, last_pos); + } + + // After validation, coalesce overlapping ranges as per RFC 9110 + coalesce_ranges(req.ranges, static_cast(content_len)); + } + + return false; +} + +inline std::pair +get_range_offset_and_length(Range r, size_t content_length) { + assert(r.first != -1 && r.second != -1); + assert(0 <= r.first && r.first < static_cast(content_length)); + assert(r.first <= r.second && + r.second < static_cast(content_length)); + (void)(content_length); + return std::make_pair(r.first, static_cast(r.second - r.first) + 1); +} + +inline std::string make_content_range_header_field( + const std::pair &offset_and_length, size_t content_length) { + auto st = offset_and_length.first; + auto ed = st + offset_and_length.second - 1; + + std::string field = "bytes "; + field += std::to_string(st); + field += "-"; + field += std::to_string(ed); + field += "/"; + field += std::to_string(content_length); + return field; +} + +template +bool process_multipart_ranges_data(const Request &req, + const std::string &boundary, + const std::string &content_type, + size_t content_length, SToken stoken, + CToken ctoken, Content content) { + for (size_t i = 0; i < req.ranges.size(); i++) { + ctoken("--"); + stoken(boundary); + ctoken("\r\n"); + if (!content_type.empty()) { + ctoken("Content-Type: "); + stoken(content_type); + ctoken("\r\n"); + } + + auto offset_and_length = + get_range_offset_and_length(req.ranges[i], content_length); + + ctoken("Content-Range: "); + stoken(make_content_range_header_field(offset_and_length, content_length)); + ctoken("\r\n"); + ctoken("\r\n"); + + if (!content(offset_and_length.first, offset_and_length.second)) { + return false; + } + ctoken("\r\n"); + } + + ctoken("--"); + stoken(boundary); + ctoken("--"); + + return true; +} + +inline void make_multipart_ranges_data(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type, + size_t content_length, + std::string &data) { + process_multipart_ranges_data( + req, boundary, content_type, content_length, + [&](const std::string &token) { data += token; }, + [&](const std::string &token) { data += token; }, + [&](size_t offset, size_t length) { + assert(offset + length <= content_length); + data += res.body.substr(offset, length); + return true; + }); +} + +inline size_t get_multipart_ranges_data_length(const Request &req, + const std::string &boundary, + const std::string &content_type, + size_t content_length) { + size_t data_length = 0; + + process_multipart_ranges_data( + req, boundary, content_type, content_length, + [&](const std::string &token) { data_length += token.size(); }, + [&](const std::string &token) { data_length += token.size(); }, + [&](size_t /*offset*/, size_t length) { + data_length += length; + return true; + }); + + return data_length; +} + +template +inline bool +write_multipart_ranges_data(Stream &strm, const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type, + size_t content_length, const T &is_shutting_down) { + return process_multipart_ranges_data( + req, boundary, content_type, content_length, + [&](const std::string &token) { strm.write(token); }, + [&](const std::string &token) { strm.write(token); }, + [&](size_t offset, size_t length) { + return write_content(strm, res.content_provider_, offset, length, + is_shutting_down); + }); +} + +inline bool expect_content(const Request &req) { + if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || + req.method == "DELETE") { + return true; + } + if (req.has_header("Content-Length") && + req.get_header_value_u64("Content-Length") > 0) { + return true; + } + if (is_chunked_transfer_encoding(req.headers)) { return true; } + return false; +} + +inline bool has_crlf(const std::string &s) { + auto p = s.c_str(); + while (*p) { + if (*p == '\r' || *p == '\n') { return true; } + p++; + } + return false; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline std::string message_digest(const std::string &s, const EVP_MD *algo) { + auto context = std::unique_ptr( + EVP_MD_CTX_new(), EVP_MD_CTX_free); + + unsigned int hash_length = 0; + unsigned char hash[EVP_MAX_MD_SIZE]; + + EVP_DigestInit_ex(context.get(), algo, nullptr); + EVP_DigestUpdate(context.get(), s.c_str(), s.size()); + EVP_DigestFinal_ex(context.get(), hash, &hash_length); + + std::stringstream ss; + for (auto i = 0u; i < hash_length; ++i) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(hash[i]); + } + + return ss.str(); +} + +inline std::string MD5(const std::string &s) { + return message_digest(s, EVP_md5()); +} + +inline std::string SHA_256(const std::string &s) { + return message_digest(s, EVP_sha256()); +} + +inline std::string SHA_512(const std::string &s) { + return message_digest(s, EVP_sha512()); +} + +inline std::pair make_digest_authentication_header( + const Request &req, const std::map &auth, + size_t cnonce_count, const std::string &cnonce, const std::string &username, + const std::string &password, bool is_proxy = false) { + std::string nc; + { + std::stringstream ss; + ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count; + nc = ss.str(); + } + + std::string qop; + if (auth.find("qop") != auth.end()) { + qop = auth.at("qop"); + if (qop.find("auth-int") != std::string::npos) { + qop = "auth-int"; + } else if (qop.find("auth") != std::string::npos) { + qop = "auth"; + } else { + qop.clear(); + } + } + + std::string algo = "MD5"; + if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } + + std::string response; + { + auto H = algo == "SHA-256" ? detail::SHA_256 + : algo == "SHA-512" ? detail::SHA_512 + : detail::MD5; + + auto A1 = username + ":" + auth.at("realm") + ":" + password; + + auto A2 = req.method + ":" + req.path; + if (qop == "auth-int") { A2 += ":" + H(req.body); } + + if (qop.empty()) { + response = H(H(A1) + ":" + auth.at("nonce") + ":" + H(A2)); + } else { + response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + + ":" + qop + ":" + H(A2)); + } + } + + auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : ""; + + auto field = "Digest username=\"" + username + "\", realm=\"" + + auth.at("realm") + "\", nonce=\"" + auth.at("nonce") + + "\", uri=\"" + req.path + "\", algorithm=" + algo + + (qop.empty() ? ", response=\"" + : ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + + cnonce + "\", response=\"") + + response + "\"" + + (opaque.empty() ? "" : ", opaque=\"" + opaque + "\""); + + auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; + return std::make_pair(key, field); +} + +inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) { + detail::set_nonblocking(sock, true); + auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); + + char buf[1]; + return !SSL_peek(ssl, buf, 1) && + SSL_get_error(ssl, 0) == SSL_ERROR_ZERO_RETURN; +} + +#ifdef _WIN32 +// NOTE: This code came up with the following stackoverflow post: +// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store +inline bool load_system_certs_on_windows(X509_STORE *store) { + auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT"); + if (!hStore) { return false; } + + auto result = false; + PCCERT_CONTEXT pContext = NULL; + while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != + nullptr) { + auto encoded_cert = + static_cast(pContext->pbCertEncoded); + + auto x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded); + if (x509) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + result = true; + } + } + + CertFreeCertificateContext(pContext); + CertCloseStore(hStore, 0); + + return result; +} +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC +template +using CFObjectPtr = + std::unique_ptr::type, void (*)(CFTypeRef)>; + +inline void cf_object_ptr_deleter(CFTypeRef obj) { + if (obj) { CFRelease(obj); } +} + +inline bool retrieve_certs_from_keychain(CFObjectPtr &certs) { + CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef}; + CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll, + kCFBooleanTrue}; + + CFObjectPtr query( + CFDictionaryCreate(nullptr, reinterpret_cast(keys), values, + sizeof(keys) / sizeof(keys[0]), + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks), + cf_object_ptr_deleter); + + if (!query) { return false; } + + CFTypeRef security_items = nullptr; + if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess || + CFArrayGetTypeID() != CFGetTypeID(security_items)) { + return false; + } + + certs.reset(reinterpret_cast(security_items)); + return true; +} + +inline bool retrieve_root_certs_from_keychain(CFObjectPtr &certs) { + CFArrayRef root_security_items = nullptr; + if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) { + return false; + } + + certs.reset(root_security_items); + return true; +} + +inline bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) { + auto result = false; + for (auto i = 0; i < CFArrayGetCount(certs); ++i) { + const auto cert = reinterpret_cast( + CFArrayGetValueAtIndex(certs, i)); + + if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; } + + CFDataRef cert_data = nullptr; + if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) != + errSecSuccess) { + continue; + } + + CFObjectPtr cert_data_ptr(cert_data, cf_object_ptr_deleter); + + auto encoded_cert = static_cast( + CFDataGetBytePtr(cert_data_ptr.get())); + + auto x509 = + d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get())); + + if (x509) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + result = true; + } + } + + return result; +} + +inline bool load_system_certs_on_macos(X509_STORE *store) { + auto result = false; + CFObjectPtr certs(nullptr, cf_object_ptr_deleter); + if (retrieve_certs_from_keychain(certs) && certs) { + result = add_certs_to_x509_store(certs.get(), store); + } + + if (retrieve_root_certs_from_keychain(certs) && certs) { + result = add_certs_to_x509_store(certs.get(), store) || result; + } + + return result; +} +#endif // _WIN32 +#endif // CPPHTTPLIB_OPENSSL_SUPPORT + +#ifdef _WIN32 +class WSInit { +public: + WSInit() { + WSADATA wsaData; + if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true; + } + + ~WSInit() { + if (is_valid_) WSACleanup(); + } + + bool is_valid_ = false; +}; + +static WSInit wsinit_; +#endif + +inline bool parse_www_authenticate(const Response &res, + std::map &auth, + bool is_proxy) { + auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; + if (res.has_header(auth_key)) { + thread_local auto re = + std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); + auto s = res.get_header_value(auth_key); + auto pos = s.find(' '); + if (pos != std::string::npos) { + auto type = s.substr(0, pos); + if (type == "Basic") { + return false; + } else if (type == "Digest") { + s = s.substr(pos + 1); + auto beg = std::sregex_iterator(s.begin(), s.end(), re); + for (auto i = beg; i != std::sregex_iterator(); ++i) { + const auto &m = *i; + auto key = s.substr(static_cast(m.position(1)), + static_cast(m.length(1))); + auto val = m.length(2) > 0 + ? s.substr(static_cast(m.position(2)), + static_cast(m.length(2))) + : s.substr(static_cast(m.position(3)), + static_cast(m.length(3))); + auth[key] = val; + } + return true; + } + } + } + return false; +} + +class ContentProviderAdapter { +public: + explicit ContentProviderAdapter( + ContentProviderWithoutLength &&content_provider) + : content_provider_(content_provider) {} + + bool operator()(size_t offset, size_t, DataSink &sink) { + return content_provider_(offset, sink); + } + +private: + ContentProviderWithoutLength content_provider_; +}; + +} // namespace detail + +inline std::string hosted_at(const std::string &hostname) { + std::vector addrs; + hosted_at(hostname, addrs); + if (addrs.empty()) { return std::string(); } + return addrs[0]; +} + +inline void hosted_at(const std::string &hostname, + std::vector &addrs) { + struct addrinfo hints; + struct addrinfo *result; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + + if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, + &result, 0)) { +#if defined __linux__ && !defined __ANDROID__ + res_init(); +#endif + return; + } + auto se = detail::scope_exit([&] { freeaddrinfo(result); }); + + for (auto rp = result; rp; rp = rp->ai_next) { + const auto &addr = + *reinterpret_cast(rp->ai_addr); + std::string ip; + auto dummy = -1; + if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip, + dummy)) { + addrs.push_back(ip); + } + } +} + +inline std::string encode_uri_component(const std::string &value) { + std::ostringstream escaped; + escaped.fill('0'); + escaped << std::hex; + + for (auto c : value) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || + c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || + c == ')') { + escaped << c; + } else { + escaped << std::uppercase; + escaped << '%' << std::setw(2) + << static_cast(static_cast(c)); + escaped << std::nouppercase; + } + } + + return escaped.str(); +} + +inline std::string encode_uri(const std::string &value) { + std::ostringstream escaped; + escaped.fill('0'); + escaped << std::hex; + + for (auto c : value) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || + c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || + c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || + c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') { + escaped << c; + } else { + escaped << std::uppercase; + escaped << '%' << std::setw(2) + << static_cast(static_cast(c)); + escaped << std::nouppercase; + } + } + + return escaped.str(); +} + +inline std::string decode_uri_component(const std::string &value) { + std::string result; + + for (size_t i = 0; i < value.size(); i++) { + if (value[i] == '%' && i + 2 < value.size()) { + auto val = 0; + if (detail::from_hex_to_i(value, i + 1, 2, val)) { + result += static_cast(val); + i += 2; + } else { + result += value[i]; + } + } else { + result += value[i]; + } + } + + return result; +} + +inline std::string decode_uri(const std::string &value) { + std::string result; + + for (size_t i = 0; i < value.size(); i++) { + if (value[i] == '%' && i + 2 < value.size()) { + auto val = 0; + if (detail::from_hex_to_i(value, i + 1, 2, val)) { + result += static_cast(val); + i += 2; + } else { + result += value[i]; + } + } else { + result += value[i]; + } + } + + return result; +} + +inline std::string encode_path_component(const std::string &component) { + std::string result; + result.reserve(component.size() * 3); + + for (size_t i = 0; i < component.size(); i++) { + auto c = static_cast(component[i]); + + // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~" + if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { + result += static_cast(c); + } + // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / + // "," / ";" / "=" + else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || + c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || + c == '=') { + result += static_cast(c); + } + // Colon is allowed in path segments except first segment + else if (c == ':') { + result += static_cast(c); + } + // @ is allowed in path + else if (c == '@') { + result += static_cast(c); + } else { + result += '%'; + char hex[3]; + snprintf(hex, sizeof(hex), "%02X", c); + result.append(hex, 2); + } + } + return result; +} + +inline std::string decode_path_component(const std::string &component) { + std::string result; + result.reserve(component.size()); + + for (size_t i = 0; i < component.size(); i++) { + if (component[i] == '%' && i + 1 < component.size()) { + if (component[i + 1] == 'u') { + // Unicode %uXXXX encoding + auto val = 0; + if (detail::from_hex_to_i(component, i + 2, 4, val)) { + // 4 digits Unicode codes + char buff[4]; + size_t len = detail::to_utf8(val, buff); + if (len > 0) { result.append(buff, len); } + i += 5; // 'u0000' + } else { + result += component[i]; + } + } else { + // Standard %XX encoding + auto val = 0; + if (detail::from_hex_to_i(component, i + 1, 2, val)) { + // 2 digits hex codes + result += static_cast(val); + i += 2; // 'XX' + } else { + result += component[i]; + } + } + } else { + result += component[i]; + } + } + return result; +} + +inline std::string encode_query_component(const std::string &component, + bool space_as_plus) { + std::string result; + result.reserve(component.size() * 3); + + for (size_t i = 0; i < component.size(); i++) { + auto c = static_cast(component[i]); + + // Unreserved characters per RFC 3986 + if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { + result += static_cast(c); + } + // Space handling + else if (c == ' ') { + if (space_as_plus) { + result += '+'; + } else { + result += "%20"; + } + } + // Plus sign handling + else if (c == '+') { + if (space_as_plus) { + result += "%2B"; + } else { + result += static_cast(c); + } + } + // Query-safe sub-delimiters (excluding & and = which are query delimiters) + else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' || + c == '*' || c == ',' || c == ';') { + result += static_cast(c); + } + // Colon and @ are allowed in query + else if (c == ':' || c == '@') { + result += static_cast(c); + } + // Forward slash is allowed in query values + else if (c == '/') { + result += static_cast(c); + } + // Question mark is allowed in query values (after first ?) + else if (c == '?') { + result += static_cast(c); + } else { + result += '%'; + char hex[3]; + snprintf(hex, sizeof(hex), "%02X", c); + result.append(hex, 2); + } + } + return result; +} + +inline std::string decode_query_component(const std::string &component, + bool plus_as_space) { + std::string result; + result.reserve(component.size()); + + for (size_t i = 0; i < component.size(); i++) { + if (component[i] == '%' && i + 2 < component.size()) { + std::string hex = component.substr(i + 1, 2); + char *end; + unsigned long value = std::strtoul(hex.c_str(), &end, 16); + if (end == hex.c_str() + 2) { + result += static_cast(value); + i += 2; + } else { + result += component[i]; + } + } else if (component[i] == '+' && plus_as_space) { + result += ' '; // + becomes space in form-urlencoded + } else { + result += component[i]; + } + } + return result; +} + +inline std::string append_query_params(const std::string &path, + const Params ¶ms) { + std::string path_with_query = path; + thread_local const std::regex re("[^?]+\\?.*"); + auto delm = std::regex_match(path, re) ? '&' : '?'; + path_with_query += delm + detail::params_to_query_str(params); + return path_with_query; +} + +// Header utilities +inline std::pair +make_range_header(const Ranges &ranges) { + std::string field = "bytes="; + auto i = 0; + for (const auto &r : ranges) { + if (i != 0) { field += ", "; } + if (r.first != -1) { field += std::to_string(r.first); } + field += '-'; + if (r.second != -1) { field += std::to_string(r.second); } + i++; + } + return std::make_pair("Range", std::move(field)); +} + +inline std::pair +make_basic_authentication_header(const std::string &username, + const std::string &password, bool is_proxy) { + auto field = "Basic " + detail::base64_encode(username + ":" + password); + auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; + return std::make_pair(key, std::move(field)); +} + +inline std::pair +make_bearer_token_authentication_header(const std::string &token, + bool is_proxy = false) { + auto field = "Bearer " + token; + auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; + return std::make_pair(key, std::move(field)); +} + +// Request implementation +inline bool Request::has_header(const std::string &key) const { + return detail::has_header(headers, key); +} + +inline std::string Request::get_header_value(const std::string &key, + const char *def, size_t id) const { + return detail::get_header_value(headers, key, def, id); +} + +inline size_t Request::get_header_value_count(const std::string &key) const { + auto r = headers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Request::set_header(const std::string &key, + const std::string &val) { + if (detail::fields::is_field_name(key) && + detail::fields::is_field_value(val)) { + headers.emplace(key, val); + } +} + +inline bool Request::has_trailer(const std::string &key) const { + return trailers.find(key) != trailers.end(); +} + +inline std::string Request::get_trailer_value(const std::string &key, + size_t id) const { + auto rng = trailers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second; } + return std::string(); +} + +inline size_t Request::get_trailer_value_count(const std::string &key) const { + auto r = trailers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline bool Request::has_param(const std::string &key) const { + return params.find(key) != params.end(); +} + +inline std::string Request::get_param_value(const std::string &key, + size_t id) const { + auto rng = params.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second; } + return std::string(); +} + +inline size_t Request::get_param_value_count(const std::string &key) const { + auto r = params.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline bool Request::is_multipart_form_data() const { + const auto &content_type = get_header_value("Content-Type"); + return !content_type.rfind("multipart/form-data", 0); +} + +// Multipart FormData implementation +inline std::string MultipartFormData::get_field(const std::string &key, + size_t id) const { + auto rng = fields.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second.content; } + return std::string(); +} + +inline std::vector +MultipartFormData::get_fields(const std::string &key) const { + std::vector values; + auto rng = fields.equal_range(key); + for (auto it = rng.first; it != rng.second; it++) { + values.push_back(it->second.content); + } + return values; +} + +inline bool MultipartFormData::has_field(const std::string &key) const { + return fields.find(key) != fields.end(); +} + +inline size_t MultipartFormData::get_field_count(const std::string &key) const { + auto r = fields.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline FormData MultipartFormData::get_file(const std::string &key, + size_t id) const { + auto rng = files.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second; } + return FormData(); +} + +inline std::vector +MultipartFormData::get_files(const std::string &key) const { + std::vector values; + auto rng = files.equal_range(key); + for (auto it = rng.first; it != rng.second; it++) { + values.push_back(it->second); + } + return values; +} + +inline bool MultipartFormData::has_file(const std::string &key) const { + return files.find(key) != files.end(); +} + +inline size_t MultipartFormData::get_file_count(const std::string &key) const { + auto r = files.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +// Response implementation +inline bool Response::has_header(const std::string &key) const { + return headers.find(key) != headers.end(); +} + +inline std::string Response::get_header_value(const std::string &key, + const char *def, + size_t id) const { + return detail::get_header_value(headers, key, def, id); +} + +inline size_t Response::get_header_value_count(const std::string &key) const { + auto r = headers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Response::set_header(const std::string &key, + const std::string &val) { + if (detail::fields::is_field_name(key) && + detail::fields::is_field_value(val)) { + headers.emplace(key, val); + } +} +inline bool Response::has_trailer(const std::string &key) const { + return trailers.find(key) != trailers.end(); +} + +inline std::string Response::get_trailer_value(const std::string &key, + size_t id) const { + auto rng = trailers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second; } + return std::string(); +} + +inline size_t Response::get_trailer_value_count(const std::string &key) const { + auto r = trailers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Response::set_redirect(const std::string &url, int stat) { + if (detail::fields::is_field_value(url)) { + set_header("Location", url); + if (300 <= stat && stat < 400) { + this->status = stat; + } else { + this->status = StatusCode::Found_302; + } + } +} + +inline void Response::set_content(const char *s, size_t n, + const std::string &content_type) { + body.assign(s, n); + + auto rng = headers.equal_range("Content-Type"); + headers.erase(rng.first, rng.second); + set_header("Content-Type", content_type); +} + +inline void Response::set_content(const std::string &s, + const std::string &content_type) { + set_content(s.data(), s.size(), content_type); +} + +inline void Response::set_content(std::string &&s, + const std::string &content_type) { + body = std::move(s); + + auto rng = headers.equal_range("Content-Type"); + headers.erase(rng.first, rng.second); + set_header("Content-Type", content_type); +} + +inline void Response::set_content_provider( + size_t in_length, const std::string &content_type, ContentProvider provider, + ContentProviderResourceReleaser resource_releaser) { + set_header("Content-Type", content_type); + content_length_ = in_length; + if (in_length > 0) { content_provider_ = std::move(provider); } + content_provider_resource_releaser_ = std::move(resource_releaser); + is_chunked_content_provider_ = false; +} + +inline void Response::set_content_provider( + const std::string &content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser) { + set_header("Content-Type", content_type); + content_length_ = 0; + content_provider_ = detail::ContentProviderAdapter(std::move(provider)); + content_provider_resource_releaser_ = std::move(resource_releaser); + is_chunked_content_provider_ = false; +} + +inline void Response::set_chunked_content_provider( + const std::string &content_type, ContentProviderWithoutLength provider, + ContentProviderResourceReleaser resource_releaser) { + set_header("Content-Type", content_type); + content_length_ = 0; + content_provider_ = detail::ContentProviderAdapter(std::move(provider)); + content_provider_resource_releaser_ = std::move(resource_releaser); + is_chunked_content_provider_ = true; +} + +inline void Response::set_file_content(const std::string &path, + const std::string &content_type) { + file_content_path_ = path; + file_content_content_type_ = content_type; +} + +inline void Response::set_file_content(const std::string &path) { + file_content_path_ = path; +} + +// Result implementation +inline bool Result::has_request_header(const std::string &key) const { + return request_headers_.find(key) != request_headers_.end(); +} + +inline std::string Result::get_request_header_value(const std::string &key, + const char *def, + size_t id) const { + return detail::get_header_value(request_headers_, key, def, id); +} + +inline size_t +Result::get_request_header_value_count(const std::string &key) const { + auto r = request_headers_.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +// Stream implementation +inline ssize_t Stream::write(const char *ptr) { + return write(ptr, strlen(ptr)); +} + +inline ssize_t Stream::write(const std::string &s) { + return write(s.data(), s.size()); +} + +// BodyReader implementation +inline ssize_t detail::BodyReader::read(char *buf, size_t len) { + if (!stream) { + last_error = Error::Connection; + return -1; + } + if (eof) { return 0; } + + if (!chunked) { + // Content-Length based reading + if (bytes_read >= content_length) { + eof = true; + return 0; + } + + auto remaining = content_length - bytes_read; + auto to_read = (std::min)(len, remaining); + auto n = stream->read(buf, to_read); + + if (n < 0) { + last_error = Error::Read; + eof = true; + return n; + } + if (n == 0) { + // Unexpected EOF before content_length + last_error = Error::Read; + eof = true; + return 0; + } + + bytes_read += static_cast(n); + if (bytes_read >= content_length) { eof = true; } + return n; + } + + // Chunked transfer encoding: delegate to shared decoder instance. + if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); } + + size_t chunk_offset = 0; + size_t chunk_total = 0; + auto n = chunked_decoder->read_payload(buf, len, chunk_offset, chunk_total); + if (n < 0) { + last_error = Error::Read; + eof = true; + return n; + } + + if (n == 0) { + // Final chunk observed. Leave trailer parsing to the caller (StreamHandle). + eof = true; + return 0; + } + + bytes_read += static_cast(n); + return n; +} + +namespace detail { + +inline void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec, + time_t timeout_sec, time_t timeout_usec, + time_t &actual_timeout_sec, + time_t &actual_timeout_usec) { + auto timeout_msec = (timeout_sec * 1000) + (timeout_usec / 1000); + + auto actual_timeout_msec = + (std::min)(max_timeout_msec - duration_msec, timeout_msec); + + if (actual_timeout_msec < 0) { actual_timeout_msec = 0; } + + actual_timeout_sec = actual_timeout_msec / 1000; + actual_timeout_usec = (actual_timeout_msec % 1000) * 1000; +} + +// Socket stream implementation +inline SocketStream::SocketStream( + socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + time_t max_timeout_msec, + std::chrono::time_point start_time) + : sock_(sock), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec), + max_timeout_msec_(max_timeout_msec), start_time_(start_time), + read_buff_(read_buff_size_, 0) {} + +inline SocketStream::~SocketStream() = default; + +inline bool SocketStream::is_readable() const { + return read_buff_off_ < read_buff_content_size_; +} + +inline bool SocketStream::wait_readable() const { + if (max_timeout_msec_ <= 0) { + return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; + } + + time_t read_timeout_sec; + time_t read_timeout_usec; + calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, + read_timeout_usec_, read_timeout_sec, read_timeout_usec); + + return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; +} + +inline bool SocketStream::wait_writable() const { + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && + is_socket_alive(sock_); +} + +inline ssize_t SocketStream::read(char *ptr, size_t size) { +#ifdef _WIN32 + size = + (std::min)(size, static_cast((std::numeric_limits::max)())); +#else + size = (std::min)(size, + static_cast((std::numeric_limits::max)())); +#endif + + if (read_buff_off_ < read_buff_content_size_) { + auto remaining_size = read_buff_content_size_ - read_buff_off_; + if (size <= remaining_size) { + memcpy(ptr, read_buff_.data() + read_buff_off_, size); + read_buff_off_ += size; + return static_cast(size); + } else { + memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size); + read_buff_off_ += remaining_size; + return static_cast(remaining_size); + } + } + + if (!wait_readable()) { return -1; } + + read_buff_off_ = 0; + read_buff_content_size_ = 0; + + if (size < read_buff_size_) { + auto n = read_socket(sock_, read_buff_.data(), read_buff_size_, + CPPHTTPLIB_RECV_FLAGS); + if (n <= 0) { + return n; + } else if (n <= static_cast(size)) { + memcpy(ptr, read_buff_.data(), static_cast(n)); + return n; + } else { + memcpy(ptr, read_buff_.data(), size); + read_buff_off_ = size; + read_buff_content_size_ = static_cast(n); + return static_cast(size); + } + } else { + return read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); + } +} + +inline ssize_t SocketStream::write(const char *ptr, size_t size) { + if (!wait_writable()) { return -1; } + +#if defined(_WIN32) && !defined(_WIN64) + size = + (std::min)(size, static_cast((std::numeric_limits::max)())); +#endif + + return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); +} + +inline void SocketStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + return detail::get_remote_ip_and_port(sock_, ip, port); +} + +inline void SocketStream::get_local_ip_and_port(std::string &ip, + int &port) const { + return detail::get_local_ip_and_port(sock_, ip, port); +} + +inline socket_t SocketStream::socket() const { return sock_; } + +inline time_t SocketStream::duration() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time_) + .count(); +} + +// Buffer stream implementation +inline bool BufferStream::is_readable() const { return true; } + +inline bool BufferStream::wait_readable() const { return true; } + +inline bool BufferStream::wait_writable() const { return true; } + +inline ssize_t BufferStream::read(char *ptr, size_t size) { +#if defined(_MSC_VER) && _MSC_VER < 1910 + auto len_read = buffer._Copy_s(ptr, size, size, position); +#else + auto len_read = buffer.copy(ptr, size, position); +#endif + position += static_cast(len_read); + return static_cast(len_read); +} + +inline ssize_t BufferStream::write(const char *ptr, size_t size) { + buffer.append(ptr, size); + return static_cast(size); +} + +inline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, + int & /*port*/) const {} + +inline void BufferStream::get_local_ip_and_port(std::string & /*ip*/, + int & /*port*/) const {} + +inline socket_t BufferStream::socket() const { return 0; } + +inline time_t BufferStream::duration() const { return 0; } + +inline const std::string &BufferStream::get_buffer() const { return buffer; } + +inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) + : MatcherBase(pattern) { + constexpr const char marker[] = "/:"; + + // One past the last ending position of a path param substring + std::size_t last_param_end = 0; + +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + // Needed to ensure that parameter names are unique during matcher + // construction + // If exceptions are disabled, only last duplicate path + // parameter will be set + std::unordered_set param_name_set; +#endif + + while (true) { + const auto marker_pos = pattern.find( + marker, last_param_end == 0 ? last_param_end : last_param_end - 1); + if (marker_pos == std::string::npos) { break; } + + static_fragments_.push_back( + pattern.substr(last_param_end, marker_pos - last_param_end + 1)); + + const auto param_name_start = marker_pos + str_len(marker); + + auto sep_pos = pattern.find(separator, param_name_start); + if (sep_pos == std::string::npos) { sep_pos = pattern.length(); } + + auto param_name = + pattern.substr(param_name_start, sep_pos - param_name_start); + +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + if (param_name_set.find(param_name) != param_name_set.cend()) { + std::string msg = "Encountered path parameter '" + param_name + + "' multiple times in route pattern '" + pattern + "'."; + throw std::invalid_argument(msg); + } +#endif + + param_names_.push_back(std::move(param_name)); + + last_param_end = sep_pos + 1; + } + + if (last_param_end < pattern.length()) { + static_fragments_.push_back(pattern.substr(last_param_end)); + } +} + +inline bool PathParamsMatcher::match(Request &request) const { + request.matches = std::smatch(); + request.path_params.clear(); + request.path_params.reserve(param_names_.size()); + + // One past the position at which the path matched the pattern last time + std::size_t starting_pos = 0; + for (size_t i = 0; i < static_fragments_.size(); ++i) { + const auto &fragment = static_fragments_[i]; + + if (starting_pos + fragment.length() > request.path.length()) { + return false; + } + + // Avoid unnecessary allocation by using strncmp instead of substr + + // comparison + if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(), + fragment.length()) != 0) { + return false; + } + + starting_pos += fragment.length(); + + // Should only happen when we have a static fragment after a param + // Example: '/users/:id/subscriptions' + // The 'subscriptions' fragment here does not have a corresponding param + if (i >= param_names_.size()) { continue; } + + auto sep_pos = request.path.find(separator, starting_pos); + if (sep_pos == std::string::npos) { sep_pos = request.path.length(); } + + const auto ¶m_name = param_names_[i]; + + request.path_params.emplace( + param_name, request.path.substr(starting_pos, sep_pos - starting_pos)); + + // Mark everything up to '/' as matched + starting_pos = sep_pos + 1; + } + // Returns false if the path is longer than the pattern + return starting_pos >= request.path.length(); +} + +inline bool RegexMatcher::match(Request &request) const { + request.path_params.clear(); + return std::regex_match(request.path, request.matches, regex_); +} + +inline std::string make_host_and_port_string(const std::string &host, int port, + bool is_ssl) { + std::string result; + + // Enclose IPv6 address in brackets (but not if already enclosed) + if (host.find(':') == std::string::npos || + (!host.empty() && host[0] == '[')) { + // IPv4, hostname, or already bracketed IPv6 + result = host; + } else { + // IPv6 address without brackets + result = "[" + host + "]"; + } + + // Append port if not default + if ((!is_ssl && port == 80) || (is_ssl && port == 443)) { + ; // do nothing + } else { + result += ":" + std::to_string(port); + } + + return result; +} + +} // namespace detail + +// HTTP server implementation +inline Server::Server() + : new_task_queue( + [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif +} + +inline Server::~Server() = default; + +inline std::unique_ptr +Server::make_matcher(const std::string &pattern) { + if (pattern.find("/:") != std::string::npos) { + return detail::make_unique(pattern); + } else { + return detail::make_unique(pattern); + } +} + +inline Server &Server::Get(const std::string &pattern, Handler handler) { + get_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Post(const std::string &pattern, Handler handler) { + post_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Post(const std::string &pattern, + HandlerWithContentReader handler) { + post_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Put(const std::string &pattern, Handler handler) { + put_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Put(const std::string &pattern, + HandlerWithContentReader handler) { + put_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Patch(const std::string &pattern, Handler handler) { + patch_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Patch(const std::string &pattern, + HandlerWithContentReader handler) { + patch_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Delete(const std::string &pattern, Handler handler) { + delete_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Delete(const std::string &pattern, + HandlerWithContentReader handler) { + delete_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Options(const std::string &pattern, Handler handler) { + options_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline bool Server::set_base_dir(const std::string &dir, + const std::string &mount_point) { + return set_mount_point(mount_point, dir); +} + +inline bool Server::set_mount_point(const std::string &mount_point, + const std::string &dir, Headers headers) { + detail::FileStat stat(dir); + if (stat.is_dir()) { + std::string mnt = !mount_point.empty() ? mount_point : "/"; + if (!mnt.empty() && mnt[0] == '/') { + base_dirs_.push_back({mnt, dir, std::move(headers)}); + return true; + } + } + return false; +} + +inline bool Server::remove_mount_point(const std::string &mount_point) { + for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { + if (it->mount_point == mount_point) { + base_dirs_.erase(it); + return true; + } + } + return false; +} + +inline Server & +Server::set_file_extension_and_mimetype_mapping(const std::string &ext, + const std::string &mime) { + file_extension_and_mimetype_map_[ext] = mime; + return *this; +} + +inline Server &Server::set_default_file_mimetype(const std::string &mime) { + default_file_mimetype_ = mime; + return *this; +} + +inline Server &Server::set_file_request_handler(Handler handler) { + file_request_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_error_handler_core(HandlerWithResponse handler, + std::true_type) { + error_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_error_handler_core(Handler handler, + std::false_type) { + error_handler_ = [handler](const Request &req, Response &res) { + handler(req, res); + return HandlerResponse::Handled; + }; + return *this; +} + +inline Server &Server::set_exception_handler(ExceptionHandler handler) { + exception_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { + pre_routing_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_post_routing_handler(Handler handler) { + post_routing_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) { + pre_request_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_logger(Logger logger) { + logger_ = std::move(logger); + return *this; +} + +inline Server &Server::set_error_logger(ErrorLogger error_logger) { + error_logger_ = std::move(error_logger); + return *this; +} + +inline Server &Server::set_pre_compression_logger(Logger logger) { + pre_compression_logger_ = std::move(logger); + return *this; +} + +inline Server & +Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { + expect_100_continue_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_address_family(int family) { + address_family_ = family; + return *this; +} + +inline Server &Server::set_tcp_nodelay(bool on) { + tcp_nodelay_ = on; + return *this; +} + +inline Server &Server::set_ipv6_v6only(bool on) { + ipv6_v6only_ = on; + return *this; +} + +inline Server &Server::set_socket_options(SocketOptions socket_options) { + socket_options_ = std::move(socket_options); + return *this; +} + +inline Server &Server::set_default_headers(Headers headers) { + default_headers_ = std::move(headers); + return *this; +} + +inline Server &Server::set_header_writer( + std::function const &writer) { + header_writer_ = writer; + return *this; +} + +inline Server & +Server::set_trusted_proxies(const std::vector &proxies) { + trusted_proxies_ = proxies; + return *this; +} + +inline Server &Server::set_keep_alive_max_count(size_t count) { + keep_alive_max_count_ = count; + return *this; +} + +inline Server &Server::set_keep_alive_timeout(time_t sec) { + keep_alive_timeout_sec_ = sec; + return *this; +} + +inline Server &Server::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; + return *this; +} + +inline Server &Server::set_write_timeout(time_t sec, time_t usec) { + write_timeout_sec_ = sec; + write_timeout_usec_ = usec; + return *this; +} + +inline Server &Server::set_idle_interval(time_t sec, time_t usec) { + idle_interval_sec_ = sec; + idle_interval_usec_ = usec; + return *this; +} + +inline Server &Server::set_payload_max_length(size_t length) { + payload_max_length_ = length; + return *this; +} + +inline bool Server::bind_to_port(const std::string &host, int port, + int socket_flags) { + auto ret = bind_internal(host, port, socket_flags); + if (ret == -1) { is_decommissioned = true; } + return ret >= 0; +} +inline int Server::bind_to_any_port(const std::string &host, int socket_flags) { + auto ret = bind_internal(host, 0, socket_flags); + if (ret == -1) { is_decommissioned = true; } + return ret; +} + +inline bool Server::listen_after_bind() { return listen_internal(); } + +inline bool Server::listen(const std::string &host, int port, + int socket_flags) { + return bind_to_port(host, port, socket_flags) && listen_internal(); +} + +inline bool Server::is_running() const { return is_running_; } + +inline void Server::wait_until_ready() const { + while (!is_running_ && !is_decommissioned) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } +} + +inline void Server::stop() { + if (is_running_) { + assert(svr_sock_ != INVALID_SOCKET); + std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); + detail::shutdown_socket(sock); + detail::close_socket(sock); + } + is_decommissioned = false; +} + +inline void Server::decommission() { is_decommissioned = true; } + +inline bool Server::parse_request_line(const char *s, Request &req) const { + auto len = strlen(s); + if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; } + len -= 2; + + { + size_t count = 0; + + detail::split(s, s + len, ' ', [&](const char *b, const char *e) { + switch (count) { + case 0: req.method = std::string(b, e); break; + case 1: req.target = std::string(b, e); break; + case 2: req.version = std::string(b, e); break; + default: break; + } + count++; + }); + + if (count != 3) { return false; } + } + + thread_local const std::set methods{ + "GET", "HEAD", "POST", "PUT", "DELETE", + "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; + + if (methods.find(req.method) == methods.end()) { + output_error_log(Error::InvalidHTTPMethod, &req); + return false; + } + + if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { + output_error_log(Error::InvalidHTTPVersion, &req); + return false; + } + + { + // Skip URL fragment + for (size_t i = 0; i < req.target.size(); i++) { + if (req.target[i] == '#') { + req.target.erase(i); + break; + } + } + + detail::divide(req.target, '?', + [&](const char *lhs_data, std::size_t lhs_size, + const char *rhs_data, std::size_t rhs_size) { + req.path = + decode_path_component(std::string(lhs_data, lhs_size)); + detail::parse_query_text(rhs_data, rhs_size, req.params); + }); + } + + return true; +} + +inline bool Server::write_response(Stream &strm, bool close_connection, + Request &req, Response &res) { + // NOTE: `req.ranges` should be empty, otherwise it will be applied + // incorrectly to the error content. + req.ranges.clear(); + return write_response_core(strm, close_connection, req, res, false); +} + +inline bool Server::write_response_with_content(Stream &strm, + bool close_connection, + const Request &req, + Response &res) { + return write_response_core(strm, close_connection, req, res, true); +} + +inline bool Server::write_response_core(Stream &strm, bool close_connection, + const Request &req, Response &res, + bool need_apply_ranges) { + assert(res.status != -1); + + if (400 <= res.status && error_handler_ && + error_handler_(req, res) == HandlerResponse::Handled) { + need_apply_ranges = true; + } + + std::string content_type; + std::string boundary; + if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } + + // Prepare additional headers + if (close_connection || req.get_header_value("Connection") == "close" || + 400 <= res.status) { // Don't leave connections open after errors + res.set_header("Connection", "close"); + } else { + std::string s = "timeout="; + s += std::to_string(keep_alive_timeout_sec_); + s += ", max="; + s += std::to_string(keep_alive_max_count_); + res.set_header("Keep-Alive", s); + } + + if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) && + !res.has_header("Content-Type")) { + res.set_header("Content-Type", "text/plain"); + } + + if (res.body.empty() && !res.content_length_ && !res.content_provider_ && + !res.has_header("Content-Length")) { + res.set_header("Content-Length", "0"); + } + + if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) { + res.set_header("Accept-Ranges", "bytes"); + } + + if (post_routing_handler_) { post_routing_handler_(req, res); } + + // Response line and headers + { + detail::BufferStream bstrm; + if (!detail::write_response_line(bstrm, res.status)) { return false; } + if (!header_writer_(bstrm, res.headers)) { return false; } + + // Flush buffer + auto &data = bstrm.get_buffer(); + detail::write_data(strm, data.data(), data.size()); + } + + // Body + auto ret = true; + if (req.method != "HEAD") { + if (!res.body.empty()) { + if (!detail::write_data(strm, res.body.data(), res.body.size())) { + ret = false; + } + } else if (res.content_provider_) { + if (write_content_with_provider(strm, req, res, boundary, content_type)) { + res.content_provider_success_ = true; + } else { + ret = false; + } + } + } + + // Log + output_log(req, res); + + return ret; +} + +inline bool +Server::write_content_with_provider(Stream &strm, const Request &req, + Response &res, const std::string &boundary, + const std::string &content_type) { + auto is_shutting_down = [this]() { + return this->svr_sock_ == INVALID_SOCKET; + }; + + if (res.content_length_ > 0) { + if (req.ranges.empty()) { + return detail::write_content(strm, res.content_provider_, 0, + res.content_length_, is_shutting_down); + } else if (req.ranges.size() == 1) { + auto offset_and_length = detail::get_range_offset_and_length( + req.ranges[0], res.content_length_); + + return detail::write_content(strm, res.content_provider_, + offset_and_length.first, + offset_and_length.second, is_shutting_down); + } else { + return detail::write_multipart_ranges_data( + strm, req, res, boundary, content_type, res.content_length_, + is_shutting_down); + } + } else { + if (res.is_chunked_content_provider_) { + auto type = detail::encoding_type(req, res); + + std::unique_ptr compressor; + if (type == detail::EncodingType::Gzip) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + compressor = detail::make_unique(); +#endif + } else if (type == detail::EncodingType::Brotli) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + compressor = detail::make_unique(); +#endif + } else if (type == detail::EncodingType::Zstd) { +#ifdef CPPHTTPLIB_ZSTD_SUPPORT + compressor = detail::make_unique(); +#endif + } else { + compressor = detail::make_unique(); + } + assert(compressor != nullptr); + + return detail::write_content_chunked(strm, res.content_provider_, + is_shutting_down, *compressor); + } else { + return detail::write_content_without_length(strm, res.content_provider_, + is_shutting_down); + } + } +} + +inline bool Server::read_content(Stream &strm, Request &req, Response &res) { + FormFields::iterator cur_field; + FormFiles::iterator cur_file; + auto is_text_field = false; + size_t count = 0; + if (read_content_core( + strm, req, res, + // Regular + [&](const char *buf, size_t n) { + if (req.body.size() + n > req.body.max_size()) { return false; } + req.body.append(buf, n); + return true; + }, + // Multipart FormData + [&](const FormData &file) { + if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { + output_error_log(Error::TooManyFormDataFiles, &req); + return false; + } + + if (file.filename.empty()) { + cur_field = req.form.fields.emplace( + file.name, FormField{file.name, file.content, file.headers}); + is_text_field = true; + } else { + cur_file = req.form.files.emplace(file.name, file); + is_text_field = false; + } + return true; + }, + [&](const char *buf, size_t n) { + if (is_text_field) { + auto &content = cur_field->second.content; + if (content.size() + n > content.max_size()) { return false; } + content.append(buf, n); + } else { + auto &content = cur_file->second.content; + if (content.size() + n > content.max_size()) { return false; } + content.append(buf, n); + } + return true; + })) { + const auto &content_type = req.get_header_value("Content-Type"); + if (!content_type.find("application/x-www-form-urlencoded")) { + if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { + res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414? + output_error_log(Error::ExceedMaxPayloadSize, &req); + return false; + } + detail::parse_query_text(req.body, req.params); + } + return true; + } + return false; +} + +inline bool Server::read_content_with_content_receiver( + Stream &strm, Request &req, Response &res, ContentReceiver receiver, + FormDataHeader multipart_header, ContentReceiver multipart_receiver) { + return read_content_core(strm, req, res, std::move(receiver), + std::move(multipart_header), + std::move(multipart_receiver)); +} + +inline bool Server::read_content_core( + Stream &strm, Request &req, Response &res, ContentReceiver receiver, + FormDataHeader multipart_header, ContentReceiver multipart_receiver) const { + detail::FormDataParser multipart_form_data_parser; + ContentReceiverWithProgress out; + + if (req.is_multipart_form_data()) { + const auto &content_type = req.get_header_value("Content-Type"); + std::string boundary; + if (!detail::parse_multipart_boundary(content_type, boundary)) { + res.status = StatusCode::BadRequest_400; + output_error_log(Error::MultipartParsing, &req); + return false; + } + + multipart_form_data_parser.set_boundary(std::move(boundary)); + out = [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { + return multipart_form_data_parser.parse(buf, n, multipart_header, + multipart_receiver); + }; + } else { + out = [receiver](const char *buf, size_t n, size_t /*off*/, + size_t /*len*/) { return receiver(buf, n); }; + } + + // RFC 7230 Section 3.3.3: If this is a request message and none of the above + // are true (no Transfer-Encoding and no Content-Length), then the message + // body length is zero (no message body is present). + // + // For non-SSL builds, peek into the socket to detect clients that send a + // body without a Content-Length header (raw HTTP over TCP). If there is + // pending data that exceeds the configured payload limit, treat this as an + // oversized request and fail early (causing connection close). For SSL + // builds we cannot reliably peek the decrypted application bytes, so keep + // the original behaviour. +#if !defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(_WIN32) + if (!req.has_header("Content-Length") && + !detail::is_chunked_transfer_encoding(req.headers)) { + socket_t s = strm.socket(); + if (s != INVALID_SOCKET) { + // Peek up to payload_max_length_ + 1 bytes. If more than + // payload_max_length_ bytes are pending, reject the request. + size_t to_peek = + (payload_max_length_ > 0) + ? (std::min)(payload_max_length_ + 1, static_cast(4096)) + : 1; + std::vector peekbuf(to_peek); + ssize_t n = ::recv(s, peekbuf.data(), to_peek, MSG_PEEK); + if (n > 0 && static_cast(n) > payload_max_length_) { + // Indicate failure so connection will be closed. + return false; + } + } + return true; + } +#else + if (!req.has_header("Content-Length") && + !detail::is_chunked_transfer_encoding(req.headers)) { + return true; + } +#endif + + if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, + out, true)) { + return false; + } + + if (req.is_multipart_form_data()) { + if (!multipart_form_data_parser.is_valid()) { + res.status = StatusCode::BadRequest_400; + output_error_log(Error::MultipartParsing, &req); + return false; + } + } + + return true; +} + +inline bool Server::handle_file_request(const Request &req, Response &res) { + for (const auto &entry : base_dirs_) { + // Prefix match + if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) { + std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); + if (detail::is_valid_path(sub_path)) { + auto path = entry.base_dir + sub_path; + if (path.back() == '/') { path += "index.html"; } + + detail::FileStat stat(path); + + if (stat.is_dir()) { + res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301); + return true; + } + + if (stat.is_file()) { + for (const auto &kv : entry.headers) { + res.set_header(kv.first, kv.second); + } + + auto mm = std::make_shared(path.c_str()); + if (!mm->is_open()) { + output_error_log(Error::OpenFile, &req); + return false; + } + + res.set_content_provider( + mm->size(), + detail::find_content_type(path, file_extension_and_mimetype_map_, + default_file_mimetype_), + [mm](size_t offset, size_t length, DataSink &sink) -> bool { + sink.write(mm->data() + offset, length); + return true; + }); + + if (req.method != "HEAD" && file_request_handler_) { + file_request_handler_(req, res); + } + + return true; + } else { + output_error_log(Error::OpenFile, &req); + } + } + } + } + return false; +} + +inline socket_t +Server::create_server_socket(const std::string &host, int port, + int socket_flags, + SocketOptions socket_options) const { + return detail::create_socket( + host, std::string(), port, address_family_, socket_flags, tcp_nodelay_, + ipv6_v6only_, std::move(socket_options), + [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool { + if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { + output_error_log(Error::BindIPAddress, nullptr); + return false; + } + if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { + output_error_log(Error::Listen, nullptr); + return false; + } + return true; + }); +} + +inline int Server::bind_internal(const std::string &host, int port, + int socket_flags) { + if (is_decommissioned) { return -1; } + + if (!is_valid()) { return -1; } + + svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_); + if (svr_sock_ == INVALID_SOCKET) { return -1; } + + if (port == 0) { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + if (getsockname(svr_sock_, reinterpret_cast(&addr), + &addr_len) == -1) { + output_error_log(Error::GetSockName, nullptr); + return -1; + } + if (addr.ss_family == AF_INET) { + return ntohs(reinterpret_cast(&addr)->sin_port); + } else if (addr.ss_family == AF_INET6) { + return ntohs(reinterpret_cast(&addr)->sin6_port); + } else { + output_error_log(Error::UnsupportedAddressFamily, nullptr); + return -1; + } + } else { + return port; + } +} + +inline bool Server::listen_internal() { + if (is_decommissioned) { return false; } + + auto ret = true; + is_running_ = true; + auto se = detail::scope_exit([&]() { is_running_ = false; }); + + { + std::unique_ptr task_queue(new_task_queue()); + + while (svr_sock_ != INVALID_SOCKET) { +#ifndef _WIN32 + if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { +#endif + auto val = detail::select_read(svr_sock_, idle_interval_sec_, + idle_interval_usec_); + if (val == 0) { // Timeout + task_queue->on_idle(); + continue; + } +#ifndef _WIN32 + } +#endif + +#if defined _WIN32 + // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT, + // OVERLAPPED + socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0); +#elif defined SOCK_CLOEXEC + socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC); +#else + socket_t sock = accept(svr_sock_, nullptr, nullptr); +#endif + + if (sock == INVALID_SOCKET) { + if (errno == EMFILE) { + // The per-process limit of open file descriptors has been reached. + // Try to accept new connections after a short sleep. + std::this_thread::sleep_for(std::chrono::microseconds{1}); + continue; + } else if (errno == EINTR || errno == EAGAIN) { + continue; + } + if (svr_sock_ != INVALID_SOCKET) { + detail::close_socket(svr_sock_); + ret = false; + output_error_log(Error::Connection, nullptr); + } else { + ; // The server socket was closed by user. + } + break; + } + + detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, + read_timeout_sec_, read_timeout_usec_); + detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO, + write_timeout_sec_, write_timeout_usec_); + + if (!task_queue->enqueue( + [this, sock]() { process_and_close_socket(sock); })) { + output_error_log(Error::ResourceExhaustion, nullptr); + detail::shutdown_socket(sock); + detail::close_socket(sock); + } + } + + task_queue->shutdown(); + } + + is_decommissioned = !ret; + return ret; +} + +inline bool Server::routing(Request &req, Response &res, Stream &strm) { + if (pre_routing_handler_ && + pre_routing_handler_(req, res) == HandlerResponse::Handled) { + return true; + } + + // File handler + if ((req.method == "GET" || req.method == "HEAD") && + handle_file_request(req, res)) { + return true; + } + + if (detail::expect_content(req)) { + // Content reader handler + { + ContentReader reader( + [&](ContentReceiver receiver) { + auto result = read_content_with_content_receiver( + strm, req, res, std::move(receiver), nullptr, nullptr); + if (!result) { output_error_log(Error::Read, &req); } + return result; + }, + [&](FormDataHeader header, ContentReceiver receiver) { + auto result = read_content_with_content_receiver( + strm, req, res, nullptr, std::move(header), + std::move(receiver)); + if (!result) { output_error_log(Error::Read, &req); } + return result; + }); + + if (req.method == "POST") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + post_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "PUT") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + put_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "PATCH") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + patch_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "DELETE") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + delete_handlers_for_content_reader_)) { + return true; + } + } + } + + // Read content into `req.body` + if (!read_content(strm, req, res)) { + output_error_log(Error::Read, &req); + return false; + } + } + + // Regular handler + if (req.method == "GET" || req.method == "HEAD") { + return dispatch_request(req, res, get_handlers_); + } else if (req.method == "POST") { + return dispatch_request(req, res, post_handlers_); + } else if (req.method == "PUT") { + return dispatch_request(req, res, put_handlers_); + } else if (req.method == "DELETE") { + return dispatch_request(req, res, delete_handlers_); + } else if (req.method == "OPTIONS") { + return dispatch_request(req, res, options_handlers_); + } else if (req.method == "PATCH") { + return dispatch_request(req, res, patch_handlers_); + } + + res.status = StatusCode::BadRequest_400; + return false; +} + +inline bool Server::dispatch_request(Request &req, Response &res, + const Handlers &handlers) const { + for (const auto &x : handlers) { + const auto &matcher = x.first; + const auto &handler = x.second; + + if (matcher->match(req)) { + req.matched_route = matcher->pattern(); + if (!pre_request_handler_ || + pre_request_handler_(req, res) != HandlerResponse::Handled) { + handler(req, res); + } + return true; + } + } + return false; +} + +inline void Server::apply_ranges(const Request &req, Response &res, + std::string &content_type, + std::string &boundary) const { + if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) { + auto it = res.headers.find("Content-Type"); + if (it != res.headers.end()) { + content_type = it->second; + res.headers.erase(it); + } + + boundary = detail::make_multipart_data_boundary(); + + res.set_header("Content-Type", + "multipart/byteranges; boundary=" + boundary); + } + + auto type = detail::encoding_type(req, res); + + if (res.body.empty()) { + if (res.content_length_ > 0) { + size_t length = 0; + if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { + length = res.content_length_; + } else if (req.ranges.size() == 1) { + auto offset_and_length = detail::get_range_offset_and_length( + req.ranges[0], res.content_length_); + + length = offset_and_length.second; + + auto content_range = detail::make_content_range_header_field( + offset_and_length, res.content_length_); + res.set_header("Content-Range", content_range); + } else { + length = detail::get_multipart_ranges_data_length( + req, boundary, content_type, res.content_length_); + } + res.set_header("Content-Length", std::to_string(length)); + } else { + if (res.content_provider_) { + if (res.is_chunked_content_provider_) { + res.set_header("Transfer-Encoding", "chunked"); + if (type == detail::EncodingType::Gzip) { + res.set_header("Content-Encoding", "gzip"); + } else if (type == detail::EncodingType::Brotli) { + res.set_header("Content-Encoding", "br"); + } else if (type == detail::EncodingType::Zstd) { + res.set_header("Content-Encoding", "zstd"); + } + } + } + } + } else { + if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { + ; + } else if (req.ranges.size() == 1) { + auto offset_and_length = + detail::get_range_offset_and_length(req.ranges[0], res.body.size()); + auto offset = offset_and_length.first; + auto length = offset_and_length.second; + + auto content_range = detail::make_content_range_header_field( + offset_and_length, res.body.size()); + res.set_header("Content-Range", content_range); + + assert(offset + length <= res.body.size()); + res.body = res.body.substr(offset, length); + } else { + std::string data; + detail::make_multipart_ranges_data(req, res, boundary, content_type, + res.body.size(), data); + res.body.swap(data); + } + + if (type != detail::EncodingType::None) { + output_pre_compression_log(req, res); + + std::unique_ptr compressor; + std::string content_encoding; + + if (type == detail::EncodingType::Gzip) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + compressor = detail::make_unique(); + content_encoding = "gzip"; +#endif + } else if (type == detail::EncodingType::Brotli) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + compressor = detail::make_unique(); + content_encoding = "br"; +#endif + } else if (type == detail::EncodingType::Zstd) { +#ifdef CPPHTTPLIB_ZSTD_SUPPORT + compressor = detail::make_unique(); + content_encoding = "zstd"; +#endif + } + + if (compressor) { + std::string compressed; + if (compressor->compress(res.body.data(), res.body.size(), true, + [&](const char *data, size_t data_len) { + compressed.append(data, data_len); + return true; + })) { + res.body.swap(compressed); + res.set_header("Content-Encoding", content_encoding); + } + } + } + + auto length = std::to_string(res.body.size()); + res.set_header("Content-Length", length); + } +} + +inline bool Server::dispatch_request_for_content_reader( + Request &req, Response &res, ContentReader content_reader, + const HandlersForContentReader &handlers) const { + for (const auto &x : handlers) { + const auto &matcher = x.first; + const auto &handler = x.second; + + if (matcher->match(req)) { + req.matched_route = matcher->pattern(); + if (!pre_request_handler_ || + pre_request_handler_(req, res) != HandlerResponse::Handled) { + handler(req, res, content_reader); + } + return true; + } + } + return false; +} + +inline std::string +get_client_ip(const std::string &x_forwarded_for, + const std::vector &trusted_proxies) { + // X-Forwarded-For is a comma-separated list per RFC 7239 + std::vector ip_list; + detail::split(x_forwarded_for.data(), + x_forwarded_for.data() + x_forwarded_for.size(), ',', + [&](const char *b, const char *e) { + auto r = detail::trim(b, e, 0, static_cast(e - b)); + ip_list.emplace_back(std::string(b + r.first, b + r.second)); + }); + + for (size_t i = 0; i < ip_list.size(); ++i) { + auto ip = ip_list[i]; + + auto is_trusted_proxy = + std::any_of(trusted_proxies.begin(), trusted_proxies.end(), + [&](const std::string &proxy) { return ip == proxy; }); + + if (is_trusted_proxy) { + if (i == 0) { + // If the trusted proxy is the first IP, there's no preceding client IP + return ip; + } else { + // Return the IP immediately before the trusted proxy + return ip_list[i - 1]; + } + } + } + + // If no trusted proxy is found, return the first IP in the list + return ip_list.front(); +} + +inline bool +Server::process_request(Stream &strm, const std::string &remote_addr, + int remote_port, const std::string &local_addr, + int local_port, bool close_connection, + bool &connection_closed, + const std::function &setup_request) { + std::array buf{}; + + detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); + + // Connection has been closed on client + if (!line_reader.getline()) { return false; } + + Request req; + req.start_time_ = std::chrono::steady_clock::now(); + + Response res; + res.version = "HTTP/1.1"; + res.headers = default_headers_; + +#ifdef __APPLE__ + // Socket file descriptor exceeded FD_SETSIZE... + if (strm.socket() >= FD_SETSIZE) { + Headers dummy; + detail::read_headers(strm, dummy); + res.status = StatusCode::InternalServerError_500; + output_error_log(Error::ExceedMaxSocketDescriptorCount, &req); + return write_response(strm, close_connection, req, res); + } +#endif + + // Request line and headers + if (!parse_request_line(line_reader.ptr(), req)) { + res.status = StatusCode::BadRequest_400; + output_error_log(Error::InvalidRequestLine, &req); + return write_response(strm, close_connection, req, res); + } + + // Request headers + if (!detail::read_headers(strm, req.headers)) { + res.status = StatusCode::BadRequest_400; + output_error_log(Error::InvalidHeaders, &req); + return write_response(strm, close_connection, req, res); + } + + // Check if the request URI doesn't exceed the limit + if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { + res.status = StatusCode::UriTooLong_414; + output_error_log(Error::ExceedUriMaxLength, &req); + return write_response(strm, close_connection, req, res); + } + + if (req.get_header_value("Connection") == "close") { + connection_closed = true; + } + + if (req.version == "HTTP/1.0" && + req.get_header_value("Connection") != "Keep-Alive") { + connection_closed = true; + } + + if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) { + auto x_forwarded_for = req.get_header_value("X-Forwarded-For"); + req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_); + } else { + req.remote_addr = remote_addr; + } + req.remote_port = remote_port; + + req.local_addr = local_addr; + req.local_port = local_port; + + if (req.has_header("Accept")) { + const auto &accept_header = req.get_header_value("Accept"); + if (!detail::parse_accept_header(accept_header, req.accept_content_types)) { + res.status = StatusCode::BadRequest_400; + output_error_log(Error::HTTPParsing, &req); + return write_response(strm, close_connection, req, res); + } + } + + if (req.has_header("Range")) { + const auto &range_header_value = req.get_header_value("Range"); + if (!detail::parse_range_header(range_header_value, req.ranges)) { + res.status = StatusCode::RangeNotSatisfiable_416; + output_error_log(Error::InvalidRangeHeader, &req); + return write_response(strm, close_connection, req, res); + } + } + + if (setup_request) { setup_request(req); } + + if (req.get_header_value("Expect") == "100-continue") { + int status = StatusCode::Continue_100; + if (expect_100_continue_handler_) { + status = expect_100_continue_handler_(req, res); + } + switch (status) { + case StatusCode::Continue_100: + case StatusCode::ExpectationFailed_417: + detail::write_response_line(strm, status); + strm.write("\r\n"); + break; + default: + connection_closed = true; + return write_response(strm, true, req, res); + } + } + + // Setup `is_connection_closed` method + auto sock = strm.socket(); + req.is_connection_closed = [sock]() { + return !detail::is_socket_alive(sock); + }; + + // Routing + auto routed = false; +#ifdef CPPHTTPLIB_NO_EXCEPTIONS + routed = routing(req, res, strm); +#else + try { + routed = routing(req, res, strm); + } catch (std::exception &e) { + if (exception_handler_) { + auto ep = std::current_exception(); + exception_handler_(req, res, ep); + routed = true; + } else { + res.status = StatusCode::InternalServerError_500; + std::string val; + auto s = e.what(); + for (size_t i = 0; s[i]; i++) { + switch (s[i]) { + case '\r': val += "\\r"; break; + case '\n': val += "\\n"; break; + default: val += s[i]; break; + } + } + res.set_header("EXCEPTION_WHAT", val); + } + } catch (...) { + if (exception_handler_) { + auto ep = std::current_exception(); + exception_handler_(req, res, ep); + routed = true; + } else { + res.status = StatusCode::InternalServerError_500; + res.set_header("EXCEPTION_WHAT", "UNKNOWN"); + } + } +#endif + if (routed) { + if (res.status == -1) { + res.status = req.ranges.empty() ? StatusCode::OK_200 + : StatusCode::PartialContent_206; + } + + // Serve file content by using a content provider + if (!res.file_content_path_.empty()) { + const auto &path = res.file_content_path_; + auto mm = std::make_shared(path.c_str()); + if (!mm->is_open()) { + res.body.clear(); + res.content_length_ = 0; + res.content_provider_ = nullptr; + res.status = StatusCode::NotFound_404; + output_error_log(Error::OpenFile, &req); + return write_response(strm, close_connection, req, res); + } + + auto content_type = res.file_content_content_type_; + if (content_type.empty()) { + content_type = detail::find_content_type( + path, file_extension_and_mimetype_map_, default_file_mimetype_); + } + + res.set_content_provider( + mm->size(), content_type, + [mm](size_t offset, size_t length, DataSink &sink) -> bool { + sink.write(mm->data() + offset, length); + return true; + }); + } + + if (detail::range_error(req, res)) { + res.body.clear(); + res.content_length_ = 0; + res.content_provider_ = nullptr; + res.status = StatusCode::RangeNotSatisfiable_416; + return write_response(strm, close_connection, req, res); + } + + return write_response_with_content(strm, close_connection, req, res); + } else { + if (res.status == -1) { res.status = StatusCode::NotFound_404; } + + return write_response(strm, close_connection, req, res); + } +} + +inline bool Server::is_valid() const { return true; } + +inline bool Server::process_and_close_socket(socket_t sock) { + std::string remote_addr; + int remote_port = 0; + detail::get_remote_ip_and_port(sock, remote_addr, remote_port); + + std::string local_addr; + int local_port = 0; + detail::get_local_ip_and_port(sock, local_addr, local_port); + + auto ret = detail::process_server_socket( + svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, remote_addr, remote_port, local_addr, + local_port, close_connection, connection_closed, + nullptr); + }); + + detail::shutdown_socket(sock); + detail::close_socket(sock); + return ret; +} + +inline void Server::output_log(const Request &req, const Response &res) const { + if (logger_) { + std::lock_guard guard(logger_mutex_); + logger_(req, res); + } +} + +inline void Server::output_pre_compression_log(const Request &req, + const Response &res) const { + if (pre_compression_logger_) { + std::lock_guard guard(logger_mutex_); + pre_compression_logger_(req, res); + } +} + +inline void Server::output_error_log(const Error &err, + const Request *req) const { + if (error_logger_) { + std::lock_guard guard(logger_mutex_); + error_logger_(err, req); + } +} + +// HTTP client implementation +inline ClientImpl::ClientImpl(const std::string &host) + : ClientImpl(host, 80, std::string(), std::string()) {} + +inline ClientImpl::ClientImpl(const std::string &host, int port) + : ClientImpl(host, port, std::string(), std::string()) {} + +inline ClientImpl::ClientImpl(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port), + host_and_port_(detail::make_host_and_port_string(host_, port, is_ssl())), + client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} + +inline ClientImpl::~ClientImpl() { + // Wait until all the requests in flight are handled. + size_t retry_count = 10; + while (retry_count-- > 0) { + { + std::lock_guard guard(socket_mutex_); + if (socket_requests_in_flight_ == 0) { break; } + } + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + + std::lock_guard guard(socket_mutex_); + shutdown_socket(socket_); + close_socket(socket_); +} + +inline bool ClientImpl::is_valid() const { return true; } + +inline void ClientImpl::copy_settings(const ClientImpl &rhs) { + client_cert_path_ = rhs.client_cert_path_; + client_key_path_ = rhs.client_key_path_; + connection_timeout_sec_ = rhs.connection_timeout_sec_; + read_timeout_sec_ = rhs.read_timeout_sec_; + read_timeout_usec_ = rhs.read_timeout_usec_; + write_timeout_sec_ = rhs.write_timeout_sec_; + write_timeout_usec_ = rhs.write_timeout_usec_; + max_timeout_msec_ = rhs.max_timeout_msec_; + basic_auth_username_ = rhs.basic_auth_username_; + basic_auth_password_ = rhs.basic_auth_password_; + bearer_token_auth_token_ = rhs.bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + digest_auth_username_ = rhs.digest_auth_username_; + digest_auth_password_ = rhs.digest_auth_password_; +#endif + keep_alive_ = rhs.keep_alive_; + follow_location_ = rhs.follow_location_; + path_encode_ = rhs.path_encode_; + address_family_ = rhs.address_family_; + tcp_nodelay_ = rhs.tcp_nodelay_; + ipv6_v6only_ = rhs.ipv6_v6only_; + socket_options_ = rhs.socket_options_; + compress_ = rhs.compress_; + decompress_ = rhs.decompress_; + interface_ = rhs.interface_; + proxy_host_ = rhs.proxy_host_; + proxy_port_ = rhs.proxy_port_; + proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; + proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; + proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; + proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + ca_cert_file_path_ = rhs.ca_cert_file_path_; + ca_cert_dir_path_ = rhs.ca_cert_dir_path_; + ca_cert_store_ = rhs.ca_cert_store_; +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + server_certificate_verification_ = rhs.server_certificate_verification_; + server_hostname_verification_ = rhs.server_hostname_verification_; + server_certificate_verifier_ = rhs.server_certificate_verifier_; +#endif + logger_ = rhs.logger_; + error_logger_ = rhs.error_logger_; +} + +inline socket_t ClientImpl::create_client_socket(Error &error) const { + if (!proxy_host_.empty() && proxy_port_ != -1) { + return detail::create_client_socket( + proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_, + ipv6_v6only_, socket_options_, connection_timeout_sec_, + connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, interface_, error); + } + + // Check is custom IP specified for host_ + std::string ip; + auto it = addr_map_.find(host_); + if (it != addr_map_.end()) { ip = it->second; } + + return detail::create_client_socket( + host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, + socket_options_, connection_timeout_sec_, connection_timeout_usec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, interface_, error); +} + +inline bool ClientImpl::create_and_connect_socket(Socket &socket, + Error &error) { + auto sock = create_client_socket(error); + if (sock == INVALID_SOCKET) { return false; } + socket.sock = sock; + return true; +} + +inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) { + return create_and_connect_socket(socket, error); +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) { + if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; } + + if (!proxy_host_.empty() && proxy_port_ != -1) { return true; } + + if (!initialize_ssl(socket, error)) { + shutdown_socket(socket); + close_socket(socket); + return false; + } + + return true; +} +#endif + +inline void ClientImpl::shutdown_ssl(Socket & /*socket*/, + bool /*shutdown_gracefully*/) { + // If there are any requests in flight from threads other than us, then it's + // a thread-unsafe race because individual ssl* objects are not thread-safe. + assert(socket_requests_in_flight_ == 0 || + socket_requests_are_from_thread_ == std::this_thread::get_id()); +} + +inline void ClientImpl::shutdown_socket(Socket &socket) const { + if (socket.sock == INVALID_SOCKET) { return; } + detail::shutdown_socket(socket.sock); +} + +inline void ClientImpl::close_socket(Socket &socket) { + // If there are requests in flight in another thread, usually closing + // the socket will be fine and they will simply receive an error when + // using the closed socket, but it is still a bug since rarely the OS + // may reassign the socket id to be used for a new socket, and then + // suddenly they will be operating on a live socket that is different + // than the one they intended! + assert(socket_requests_in_flight_ == 0 || + socket_requests_are_from_thread_ == std::this_thread::get_id()); + + // It is also a bug if this happens while SSL is still active +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + assert(socket.ssl == nullptr); +#endif + if (socket.sock == INVALID_SOCKET) { return; } + detail::close_socket(socket.sock); + socket.sock = INVALID_SOCKET; +} + +inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, + Response &res) const { + std::array buf{}; + + detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); + + if (!line_reader.getline()) { return false; } + +#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"); +#else + thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"); +#endif + + std::cmatch m; + if (!std::regex_match(line_reader.ptr(), m, re)) { + return req.method == "CONNECT"; + } + res.version = std::string(m[1]); + res.status = std::stoi(std::string(m[2])); + res.reason = std::string(m[3]); + + // Ignore '100 Continue' + while (res.status == StatusCode::Continue_100) { + if (!line_reader.getline()) { return false; } // CRLF + if (!line_reader.getline()) { return false; } // next response line + + if (!std::regex_match(line_reader.ptr(), m, re)) { return false; } + res.version = std::string(m[1]); + res.status = std::stoi(std::string(m[2])); + res.reason = std::string(m[3]); + } + + return true; +} + +inline bool ClientImpl::send(Request &req, Response &res, Error &error) { + std::lock_guard request_mutex_guard(request_mutex_); + auto ret = send_(req, res, error); + if (error == Error::SSLPeerCouldBeClosed_) { + assert(!ret); + ret = send_(req, res, error); + } + return ret; +} + +inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { + { + std::lock_guard guard(socket_mutex_); + + // Set this to false immediately - if it ever gets set to true by the end + // of the request, we know another thread instructed us to close the + // socket. + socket_should_be_closed_when_request_is_done_ = false; + + auto is_alive = false; + if (socket_.is_open()) { + is_alive = detail::is_socket_alive(socket_.sock); + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_alive && is_ssl()) { + if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { + is_alive = false; + } + } +#endif + + if (!is_alive) { + // Attempt to avoid sigpipe by shutting down non-gracefully if it + // seems like the other side has already closed the connection Also, + // there cannot be any requests in flight from other threads since we + // locked request_mutex_, so safe to close everything immediately + const bool shutdown_gracefully = false; + shutdown_ssl(socket_, shutdown_gracefully); + shutdown_socket(socket_); + close_socket(socket_); + } + } + + if (!is_alive) { + if (!ensure_socket_connection(socket_, error)) { + output_error_log(error, &req); + return false; + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // TODO: refactoring + if (is_ssl()) { + auto &scli = static_cast(*this); + if (!proxy_host_.empty() && proxy_port_ != -1) { + auto success = false; + if (!scli.connect_with_proxy(socket_, req.start_time_, res, success, + error)) { + if (!success) { output_error_log(error, &req); } + return success; + } + } + + if (!proxy_host_.empty() && proxy_port_ != -1) { + if (!scli.initialize_ssl(socket_, error)) { + output_error_log(error, &req); + return false; + } + } + } +#endif + } + + // Mark the current socket as being in use so that it cannot be closed by + // anyone else while this request is ongoing, even though we will be + // releasing the mutex. + if (socket_requests_in_flight_ > 1) { + assert(socket_requests_are_from_thread_ == std::this_thread::get_id()); + } + socket_requests_in_flight_ += 1; + socket_requests_are_from_thread_ = std::this_thread::get_id(); + } + + for (const auto &header : default_headers_) { + if (req.headers.find(header.first) == req.headers.end()) { + req.headers.insert(header); + } + } + + auto ret = false; + auto close_connection = !keep_alive_; + + auto se = detail::scope_exit([&]() { + // Briefly lock mutex in order to mark that a request is no longer ongoing + std::lock_guard guard(socket_mutex_); + socket_requests_in_flight_ -= 1; + if (socket_requests_in_flight_ <= 0) { + assert(socket_requests_in_flight_ == 0); + socket_requests_are_from_thread_ = std::thread::id(); + } + + if (socket_should_be_closed_when_request_is_done_ || close_connection || + !ret) { + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); + } + }); + + ret = process_socket(socket_, req.start_time_, [&](Stream &strm) { + return handle_request(strm, req, res, close_connection, error); + }); + + if (!ret) { + if (error == Error::Success) { + error = Error::Unknown; + output_error_log(error, &req); + } + } + + return ret; +} + +inline Result ClientImpl::send(const Request &req) { + auto req2 = req; + return send_(std::move(req2)); +} + +inline Result ClientImpl::send_(Request &&req) { + auto res = detail::make_unique(); + auto error = Error::Success; + auto ret = send(req, *res, error); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), + last_ssl_error_, last_openssl_error_}; +#else + return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; +#endif +} + +inline void ClientImpl::prepare_default_headers(Request &r, bool for_stream, + const std::string &ct) { + (void)for_stream; + for (const auto &header : default_headers_) { + if (!r.has_header(header.first)) { r.headers.insert(header); } + } + + if (!r.has_header("Host")) { + if (address_family_ == AF_UNIX) { + r.headers.emplace("Host", "localhost"); + } else { + r.headers.emplace("Host", host_and_port_); + } + } + + if (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); } + + if (!r.content_receiver) { + if (!r.has_header("Accept-Encoding")) { + std::string accept_encoding; +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + accept_encoding = "br"; +#endif +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (!accept_encoding.empty()) { accept_encoding += ", "; } + accept_encoding += "gzip, deflate"; +#endif +#ifdef CPPHTTPLIB_ZSTD_SUPPORT + if (!accept_encoding.empty()) { accept_encoding += ", "; } + accept_encoding += "zstd"; +#endif + r.set_header("Accept-Encoding", accept_encoding); + } + +#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT + if (!r.has_header("User-Agent")) { + auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; + r.set_header("User-Agent", agent); + } +#endif + } + + if (!r.body.empty()) { + if (!ct.empty() && !r.has_header("Content-Type")) { + r.headers.emplace("Content-Type", ct); + } + if (!r.has_header("Content-Length")) { + r.headers.emplace("Content-Length", std::to_string(r.body.size())); + } + } +} + +inline ClientImpl::StreamHandle +ClientImpl::open_stream(const std::string &method, const std::string &path, + const Params ¶ms, const Headers &headers, + const std::string &body, + const std::string &content_type) { + StreamHandle handle; + handle.response = detail::make_unique(); + handle.error = Error::Success; + + auto query_path = params.empty() ? path : append_query_params(path, params); + handle.connection_ = detail::make_unique(); + + { + std::lock_guard guard(socket_mutex_); + + auto is_alive = false; + if (socket_.is_open()) { + is_alive = detail::is_socket_alive(socket_.sock); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_alive && is_ssl()) { + if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { + is_alive = false; + } + } +#endif + if (!is_alive) { + shutdown_ssl(socket_, false); + shutdown_socket(socket_); + close_socket(socket_); + } + } + + if (!is_alive) { + if (!ensure_socket_connection(socket_, handle.error)) { + handle.response.reset(); + return handle; + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_ssl()) { + auto &scli = static_cast(*this); + if (!proxy_host_.empty() && proxy_port_ != -1) { + if (!scli.initialize_ssl(socket_, handle.error)) { + handle.response.reset(); + return handle; + } + } + } +#endif + } + + transfer_socket_ownership_to_handle(handle); + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_ssl() && handle.connection_->ssl) { + handle.socket_stream_ = detail::make_unique( + handle.connection_->sock, handle.connection_->ssl, read_timeout_sec_, + read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); + } else { + handle.socket_stream_ = detail::make_unique( + handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_); + } +#else + handle.socket_stream_ = detail::make_unique( + handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_); +#endif + handle.stream_ = handle.socket_stream_.get(); + + Request req; + req.method = method; + req.path = query_path; + req.headers = headers; + req.body = body; + + prepare_default_headers(req, true, content_type); + + auto &strm = *handle.stream_; + if (detail::write_request_line(strm, req.method, req.path) < 0) { + handle.error = Error::Write; + handle.response.reset(); + return handle; + } + + if (!detail::write_headers(strm, req.headers)) { + handle.error = Error::Write; + handle.response.reset(); + return handle; + } + + if (!body.empty()) { + if (strm.write(body.data(), body.size()) < 0) { + handle.error = Error::Write; + handle.response.reset(); + return handle; + } + } + + if (!read_response_line(strm, req, *handle.response) || + !detail::read_headers(strm, handle.response->headers)) { + handle.error = Error::Read; + handle.response.reset(); + return handle; + } + + handle.body_reader_.stream = handle.stream_; + + auto content_length_str = handle.response->get_header_value("Content-Length"); + if (!content_length_str.empty()) { + handle.body_reader_.content_length = + static_cast(std::stoull(content_length_str)); + } + + auto transfer_encoding = + handle.response->get_header_value("Transfer-Encoding"); + handle.body_reader_.chunked = (transfer_encoding == "chunked"); + + auto content_encoding = handle.response->get_header_value("Content-Encoding"); + if (!content_encoding.empty()) { + handle.decompressor_ = detail::create_decompressor(content_encoding); + } + + return handle; +} + +inline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) { + if (!is_valid() || !response) { return -1; } + + if (decompressor_) { return read_with_decompression(buf, len); } + auto n = detail::read_body_content(stream_, body_reader_, buf, len); + + if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) { + trailers_parsed_ = true; + if (body_reader_.chunked_decoder) { + if (!body_reader_.chunked_decoder->parse_trailers_into( + response->trailers, response->headers)) { + return n; + } + } else { + detail::ChunkedDecoder dec(*stream_); + if (!dec.parse_trailers_into(response->trailers, response->headers)) { + return n; + } + } + } + + return n; +} + +inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, + size_t len) { + if (decompress_offset_ < decompress_buffer_.size()) { + auto available = decompress_buffer_.size() - decompress_offset_; + auto to_copy = (std::min)(len, available); + std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy); + decompress_offset_ += to_copy; + return static_cast(to_copy); + } + + decompress_buffer_.clear(); + decompress_offset_ = 0; + + constexpr size_t kDecompressionBufferSize = 8192; + char compressed_buf[kDecompressionBufferSize]; + + while (true) { + auto n = detail::read_body_content(stream_, body_reader_, compressed_buf, + sizeof(compressed_buf)); + + if (n <= 0) { return n; } + + bool decompress_ok = + decompressor_->decompress(compressed_buf, static_cast(n), + [this](const char *data, size_t data_len) { + decompress_buffer_.append(data, data_len); + return true; + }); + + if (!decompress_ok) { + body_reader_.last_error = Error::Read; + return -1; + } + + if (!decompress_buffer_.empty()) { break; } + } + + auto to_copy = (std::min)(len, decompress_buffer_.size()); + std::memcpy(buf, decompress_buffer_.data(), to_copy); + decompress_offset_ = to_copy; + return static_cast(to_copy); +} + +inline void ClientImpl::StreamHandle::parse_trailers_if_needed() { + if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) { + return; + } + + trailers_parsed_ = true; + + const auto bufsiz = 128; + char line_buf[bufsiz]; + detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz); + + if (!line_reader.getline()) { return; } + + if (!detail::parse_trailers(line_reader, response->trailers, + response->headers)) { + return; + } +} + +// Inline method implementations for `ChunkedDecoder`. +namespace detail { + +inline ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {} + +inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len, + size_t &out_chunk_offset, + size_t &out_chunk_total) { + if (finished) { return 0; } + + if (chunk_remaining == 0) { + stream_line_reader lr(strm, line_buf, sizeof(line_buf)); + if (!lr.getline()) { return -1; } + + char *endptr = nullptr; + unsigned long chunk_len = std::strtoul(lr.ptr(), &endptr, 16); + if (endptr == lr.ptr()) { return -1; } + if (chunk_len == ULONG_MAX) { return -1; } + + if (chunk_len == 0) { + chunk_remaining = 0; + finished = true; + out_chunk_offset = 0; + out_chunk_total = 0; + return 0; + } + + chunk_remaining = static_cast(chunk_len); + last_chunk_total = chunk_remaining; + last_chunk_offset = 0; + } + + auto to_read = (std::min)(chunk_remaining, len); + auto n = strm.read(buf, to_read); + if (n <= 0) { return -1; } + + auto offset_before = last_chunk_offset; + last_chunk_offset += static_cast(n); + chunk_remaining -= static_cast(n); + + out_chunk_offset = offset_before; + out_chunk_total = last_chunk_total; + + if (chunk_remaining == 0) { + stream_line_reader lr(strm, line_buf, sizeof(line_buf)); + if (!lr.getline()) { return -1; } + if (std::strcmp(lr.ptr(), "\r\n") != 0) { return -1; } + } + + return n; +} + +inline bool ChunkedDecoder::parse_trailers_into(Headers &dest, + const Headers &src_headers) { + stream_line_reader lr(strm, line_buf, sizeof(line_buf)); + if (!lr.getline()) { return false; } + return parse_trailers(lr, dest, src_headers); +} + +} // namespace detail + +inline void +ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) { + handle.connection_->sock = socket_.sock; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + handle.connection_->ssl = socket_.ssl; + socket_.ssl = nullptr; +#endif + socket_.sock = INVALID_SOCKET; +} + +inline bool ClientImpl::handle_request(Stream &strm, Request &req, + Response &res, bool close_connection, + Error &error) { + if (req.path.empty()) { + error = Error::Connection; + output_error_log(error, &req); + return false; + } + + auto req_save = req; + + bool ret; + + if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) { + auto req2 = req; + req2.path = "http://" + host_and_port_ + req.path; + ret = process_request(strm, req2, res, close_connection, error); + req = req2; + req.path = req_save.path; + } else { + ret = process_request(strm, req, res, close_connection, error); + } + + if (!ret) { return false; } + + if (res.get_header_value("Connection") == "close" || + (res.version == "HTTP/1.0" && res.reason != "Connection established")) { + // TODO this requires a not-entirely-obvious chain of calls to be correct + // for this to be safe. + + // This is safe to call because handle_request is only called by send_ + // which locks the request mutex during the process. It would be a bug + // to call it from a different thread since it's a thread-safety issue + // to do these things to the socket if another thread is using the socket. + std::lock_guard guard(socket_mutex_); + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); + } + + if (300 < res.status && res.status < 400 && follow_location_) { + req = req_save; + ret = redirect(req, res, error); + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if ((res.status == StatusCode::Unauthorized_401 || + res.status == StatusCode::ProxyAuthenticationRequired_407) && + req.authorization_count_ < 5) { + auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407; + const auto &username = + is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; + const auto &password = + is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; + + if (!username.empty() && !password.empty()) { + std::map auth; + if (detail::parse_www_authenticate(res, auth, is_proxy)) { + Request new_req = req; + new_req.authorization_count_ += 1; + new_req.headers.erase(is_proxy ? "Proxy-Authorization" + : "Authorization"); + new_req.headers.insert(detail::make_digest_authentication_header( + req, auth, new_req.authorization_count_, detail::random_string(10), + username, password, is_proxy)); + + Response new_res; + + ret = send(new_req, new_res, error); + if (ret) { res = new_res; } + } + } + } +#endif + + return ret; +} + +inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { + if (req.redirect_count_ == 0) { + error = Error::ExceedRedirectCount; + output_error_log(error, &req); + return false; + } + + auto location = res.get_header_value("location"); + if (location.empty()) { return false; } + + thread_local const std::regex re( + R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"); + + std::smatch m; + if (!std::regex_match(location, m, re)) { return false; } + + auto scheme = is_ssl() ? "https" : "http"; + + auto next_scheme = m[1].str(); + auto next_host = m[2].str(); + if (next_host.empty()) { next_host = m[3].str(); } + auto port_str = m[4].str(); + auto next_path = m[5].str(); + auto next_query = m[6].str(); + + auto next_port = port_; + if (!port_str.empty()) { + next_port = std::stoi(port_str); + } else if (!next_scheme.empty()) { + next_port = next_scheme == "https" ? 443 : 80; + } + + if (next_scheme.empty()) { next_scheme = scheme; } + if (next_host.empty()) { next_host = host_; } + if (next_path.empty()) { next_path = "/"; } + + auto path = decode_query_component(next_path, true) + next_query; + + // Same host redirect - use current client + if (next_scheme == scheme && next_host == host_ && next_port == port_) { + return detail::redirect(*this, req, res, path, location, error); + } + + // Cross-host/scheme redirect - create new client with robust setup + return create_redirect_client(next_scheme, next_host, next_port, req, res, + path, location, error); +} + +// New method for robust redirect client creation +inline bool ClientImpl::create_redirect_client( + const std::string &scheme, const std::string &host, int port, Request &req, + Response &res, const std::string &path, const std::string &location, + Error &error) { + // Determine if we need SSL + auto need_ssl = (scheme == "https"); + + // Clean up request headers that are host/client specific + // Remove headers that should not be carried over to new host + auto headers_to_remove = + std::vector{"Host", "Proxy-Authorization", "Authorization"}; + + for (const auto &header_name : headers_to_remove) { + auto it = req.headers.find(header_name); + while (it != req.headers.end()) { + it = req.headers.erase(it); + it = req.headers.find(header_name); + } + } + + // Create appropriate client type and handle redirect + if (need_ssl) { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Create SSL client for HTTPS redirect + SSLClient redirect_client(host, port); + + // Setup basic client configuration first + setup_redirect_client(redirect_client); + + // SSL-specific configuration for proxy environments + if (!proxy_host_.empty() && proxy_port_ != -1) { + // Critical: Disable SSL verification for proxy environments + redirect_client.enable_server_certificate_verification(false); + redirect_client.enable_server_hostname_verification(false); + } else { + // For direct SSL connections, copy SSL verification settings + redirect_client.enable_server_certificate_verification( + server_certificate_verification_); + redirect_client.enable_server_hostname_verification( + server_hostname_verification_); + } + + // Handle CA certificate store and paths if available + if (ca_cert_store_ && X509_STORE_up_ref(ca_cert_store_)) { + redirect_client.set_ca_cert_store(ca_cert_store_); + } + if (!ca_cert_file_path_.empty()) { + redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); + } + + // Client certificates are set through constructor for SSLClient + // NOTE: SSLClient constructor already takes client_cert_path and + // client_key_path so we need to create it properly if client certs are + // needed + + // Execute the redirect + return detail::redirect(redirect_client, req, res, path, location, error); +#else + // SSL not supported - set appropriate error + error = Error::SSLConnection; + output_error_log(error, &req); + return false; +#endif + } else { + // HTTP redirect + ClientImpl redirect_client(host, port); + + // Setup client with robust configuration + setup_redirect_client(redirect_client); + + // Execute the redirect + return detail::redirect(redirect_client, req, res, path, location, error); + } +} + +// New method for robust client setup (based on basic_manual_redirect.cpp +// logic) +template +inline void ClientImpl::setup_redirect_client(ClientType &client) { + // Copy basic settings first + client.set_connection_timeout(connection_timeout_sec_); + client.set_read_timeout(read_timeout_sec_, read_timeout_usec_); + client.set_write_timeout(write_timeout_sec_, write_timeout_usec_); + client.set_keep_alive(keep_alive_); + client.set_follow_location( + true); // Enable redirects to handle multi-step redirects + client.set_path_encode(path_encode_); + client.set_compress(compress_); + client.set_decompress(decompress_); + + // Copy authentication settings BEFORE proxy setup + if (!basic_auth_username_.empty()) { + client.set_basic_auth(basic_auth_username_, basic_auth_password_); + } + if (!bearer_token_auth_token_.empty()) { + client.set_bearer_token_auth(bearer_token_auth_token_); + } +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (!digest_auth_username_.empty()) { + client.set_digest_auth(digest_auth_username_, digest_auth_password_); + } +#endif + + // Setup proxy configuration (CRITICAL ORDER - proxy must be set + // before proxy auth) + if (!proxy_host_.empty() && proxy_port_ != -1) { + // First set proxy host and port + client.set_proxy(proxy_host_, proxy_port_); + + // Then set proxy authentication (order matters!) + if (!proxy_basic_auth_username_.empty()) { + client.set_proxy_basic_auth(proxy_basic_auth_username_, + proxy_basic_auth_password_); + } + if (!proxy_bearer_token_auth_token_.empty()) { + client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_); + } +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (!proxy_digest_auth_username_.empty()) { + client.set_proxy_digest_auth(proxy_digest_auth_username_, + proxy_digest_auth_password_); + } +#endif + } + + // Copy network and socket settings + client.set_address_family(address_family_); + client.set_tcp_nodelay(tcp_nodelay_); + client.set_ipv6_v6only(ipv6_v6only_); + if (socket_options_) { client.set_socket_options(socket_options_); } + if (!interface_.empty()) { client.set_interface(interface_); } + + // Copy logging and headers + if (logger_) { client.set_logger(logger_); } + if (error_logger_) { client.set_error_logger(error_logger_); } + + // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers + // Each new client should generate its own headers based on its target host +} + +inline bool ClientImpl::write_content_with_provider(Stream &strm, + const Request &req, + Error &error) const { + auto is_shutting_down = []() { return false; }; + + if (req.is_chunked_content_provider_) { + // TODO: Brotli support + std::unique_ptr compressor; +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_) { + compressor = detail::make_unique(); + } else +#endif + { + compressor = detail::make_unique(); + } + + return detail::write_content_chunked(strm, req.content_provider_, + is_shutting_down, *compressor, error); + } else { + return detail::write_content_with_progress( + strm, req.content_provider_, 0, req.content_length_, is_shutting_down, + req.upload_progress, error); + } +} + +inline bool ClientImpl::write_request(Stream &strm, Request &req, + bool close_connection, Error &error) { + // Prepare additional headers + if (close_connection) { + if (!req.has_header("Connection")) { + req.set_header("Connection", "close"); + } + } + + std::string ct_for_defaults; + if (!req.has_header("Content-Type") && !req.body.empty()) { + ct_for_defaults = "text/plain"; + } + prepare_default_headers(req, false, ct_for_defaults); + + if (req.body.empty()) { + if (req.content_provider_) { + if (!req.is_chunked_content_provider_) { + if (!req.has_header("Content-Length")) { + auto length = std::to_string(req.content_length_); + req.set_header("Content-Length", length); + } + } + } else { + if (req.method == "POST" || req.method == "PUT" || + req.method == "PATCH") { + req.set_header("Content-Length", "0"); + } + } + } + + if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { + if (!req.has_header("Authorization")) { + req.headers.insert(make_basic_authentication_header( + basic_auth_username_, basic_auth_password_, false)); + } + } + + if (!proxy_basic_auth_username_.empty() && + !proxy_basic_auth_password_.empty()) { + if (!req.has_header("Proxy-Authorization")) { + req.headers.insert(make_basic_authentication_header( + proxy_basic_auth_username_, proxy_basic_auth_password_, true)); + } + } + + if (!bearer_token_auth_token_.empty()) { + if (!req.has_header("Authorization")) { + req.headers.insert(make_bearer_token_authentication_header( + bearer_token_auth_token_, false)); + } + } + + if (!proxy_bearer_token_auth_token_.empty()) { + if (!req.has_header("Proxy-Authorization")) { + req.headers.insert(make_bearer_token_authentication_header( + proxy_bearer_token_auth_token_, true)); + } + } + + // Request line and headers + { + detail::BufferStream bstrm; + + // Extract path and query from req.path + std::string path_part, query_part; + auto query_pos = req.path.find('?'); + if (query_pos != std::string::npos) { + path_part = req.path.substr(0, query_pos); + query_part = req.path.substr(query_pos + 1); + } else { + path_part = req.path; + query_part = ""; + } + + // Encode path and query + auto path_with_query = + path_encode_ ? detail::encode_path(path_part) : path_part; + + detail::parse_query_text(query_part, req.params); + if (!req.params.empty()) { + path_with_query = append_query_params(path_with_query, req.params); + } + + // Write request line and headers + detail::write_request_line(bstrm, req.method, path_with_query); + header_writer_(bstrm, req.headers); + + // Flush buffer + auto &data = bstrm.get_buffer(); + if (!detail::write_data(strm, data.data(), data.size())) { + error = Error::Write; + output_error_log(error, &req); + return false; + } + } + + // Body + if (req.body.empty()) { + return write_content_with_provider(strm, req, error); + } + + if (req.upload_progress) { + auto body_size = req.body.size(); + size_t written = 0; + auto data = req.body.data(); + + while (written < body_size) { + size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written); + if (!detail::write_data(strm, data + written, to_write)) { + error = Error::Write; + output_error_log(error, &req); + return false; + } + written += to_write; + + if (!req.upload_progress(written, body_size)) { + error = Error::Canceled; + output_error_log(error, &req); + return false; + } + } + } else { + if (!detail::write_data(strm, req.body.data(), req.body.size())) { + error = Error::Write; + output_error_log(error, &req); + return false; + } + } + + return true; +} + +inline std::unique_ptr +ClientImpl::send_with_content_provider_and_receiver( + Request &req, const char *body, size_t content_length, + ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const std::string &content_type, ContentReceiver content_receiver, + Error &error) { + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_) { req.set_header("Content-Encoding", "gzip"); } +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_ && !content_provider_without_length) { + // TODO: Brotli support + detail::gzip_compressor compressor; + + if (content_provider) { + auto ok = true; + size_t offset = 0; + DataSink data_sink; + + data_sink.write = [&](const char *data, size_t data_len) -> bool { + if (ok) { + auto last = offset + data_len == content_length; + + auto ret = compressor.compress( + data, data_len, last, + [&](const char *compressed_data, size_t compressed_data_len) { + req.body.append(compressed_data, compressed_data_len); + return true; + }); + + if (ret) { + offset += data_len; + } else { + ok = false; + } + } + return ok; + }; + + while (ok && offset < content_length) { + if (!content_provider(offset, content_length - offset, data_sink)) { + error = Error::Canceled; + output_error_log(error, &req); + return nullptr; + } + } + } else { + if (!compressor.compress(body, content_length, true, + [&](const char *data, size_t data_len) { + req.body.append(data, data_len); + return true; + })) { + error = Error::Compression; + output_error_log(error, &req); + return nullptr; + } + } + } else +#endif + { + if (content_provider) { + req.content_length_ = content_length; + req.content_provider_ = std::move(content_provider); + req.is_chunked_content_provider_ = false; + } else if (content_provider_without_length) { + req.content_length_ = 0; + req.content_provider_ = detail::ContentProviderAdapter( + std::move(content_provider_without_length)); + req.is_chunked_content_provider_ = true; + req.set_header("Transfer-Encoding", "chunked"); + } else { + req.body.assign(body, content_length); + } + } + + if (content_receiver) { + req.content_receiver = + [content_receiver](const char *data, size_t data_length, + size_t /*offset*/, size_t /*total_length*/) { + return content_receiver(data, data_length); + }; + } + + auto res = detail::make_unique(); + return send(req, *res, error) ? std::move(res) : nullptr; +} + +inline Result ClientImpl::send_with_content_provider_and_receiver( + const std::string &method, const std::string &path, const Headers &headers, + const char *body, size_t content_length, ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const std::string &content_type, ContentReceiver content_receiver, + UploadProgress progress) { + Request req; + req.method = method; + req.headers = headers; + req.path = path; + req.upload_progress = std::move(progress); + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + auto error = Error::Success; + + auto res = send_with_content_provider_and_receiver( + req, body, content_length, std::move(content_provider), + std::move(content_provider_without_length), content_type, + std::move(content_receiver), error); + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + return Result{std::move(res), error, std::move(req.headers), last_ssl_error_, + last_openssl_error_}; +#else + return Result{std::move(res), error, std::move(req.headers)}; +#endif +} + +inline void ClientImpl::output_log(const Request &req, + const Response &res) const { + if (logger_) { + std::lock_guard guard(logger_mutex_); + logger_(req, res); + } +} + +inline void ClientImpl::output_error_log(const Error &err, + const Request *req) const { + if (error_logger_) { + std::lock_guard guard(logger_mutex_); + error_logger_(err, req); + } +} + +inline bool ClientImpl::process_request(Stream &strm, Request &req, + Response &res, bool close_connection, + Error &error) { + // Send request + if (!write_request(strm, req, close_connection, error)) { return false; } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_ssl()) { + auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1; + if (!is_proxy_enabled) { + if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { + error = Error::SSLPeerCouldBeClosed_; + output_error_log(error, &req); + return false; + } + } + } +#endif + + // Receive response and headers + if (!read_response_line(strm, req, res) || + !detail::read_headers(strm, res.headers)) { + error = Error::Read; + output_error_log(error, &req); + return false; + } + + // Body + if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && + req.method != "CONNECT") { + auto redirect = 300 < res.status && res.status < 400 && + res.status != StatusCode::NotModified_304 && + follow_location_; + + if (req.response_handler && !redirect) { + if (!req.response_handler(res)) { + error = Error::Canceled; + output_error_log(error, &req); + return false; + } + } + + auto out = + req.content_receiver + ? static_cast( + [&](const char *buf, size_t n, size_t off, size_t len) { + if (redirect) { return true; } + auto ret = req.content_receiver(buf, n, off, len); + if (!ret) { + error = Error::Canceled; + output_error_log(error, &req); + } + return ret; + }) + : static_cast( + [&](const char *buf, size_t n, size_t /*off*/, + size_t /*len*/) { + assert(res.body.size() + n <= res.body.max_size()); + res.body.append(buf, n); + return true; + }); + + auto progress = [&](size_t current, size_t total) { + if (!req.download_progress || redirect) { return true; } + auto ret = req.download_progress(current, total); + if (!ret) { + error = Error::Canceled; + output_error_log(error, &req); + } + return ret; + }; + + if (res.has_header("Content-Length")) { + if (!req.content_receiver) { + auto len = res.get_header_value_u64("Content-Length"); + if (len > res.body.max_size()) { + error = Error::Read; + output_error_log(error, &req); + return false; + } + res.body.reserve(static_cast(len)); + } + } + + if (res.status != StatusCode::NotModified_304) { + int dummy_status; + if (!detail::read_content(strm, res, (std::numeric_limits::max)(), + dummy_status, std::move(progress), + std::move(out), decompress_)) { + if (error != Error::Canceled) { error = Error::Read; } + output_error_log(error, &req); + return false; + } + } + } + + // Log + output_log(req, res); + + return true; +} + +inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( + const std::string &boundary, const UploadFormDataItems &items, + const FormDataProviderItems &provider_items) const { + size_t cur_item = 0; + size_t cur_start = 0; + // cur_item and cur_start are copied to within the std::function and + // maintain state between successive calls + return [&, cur_item, cur_start](size_t offset, + DataSink &sink) mutable -> bool { + if (!offset && !items.empty()) { + sink.os << detail::serialize_multipart_formdata(items, boundary, false); + return true; + } else if (cur_item < provider_items.size()) { + if (!cur_start) { + const auto &begin = detail::serialize_multipart_formdata_item_begin( + provider_items[cur_item], boundary); + offset += begin.size(); + cur_start = offset; + sink.os << begin; + } + + DataSink cur_sink; + auto has_data = true; + cur_sink.write = sink.write; + cur_sink.done = [&]() { has_data = false; }; + + if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) { + return false; + } + + if (!has_data) { + sink.os << detail::serialize_multipart_formdata_item_end(); + cur_item++; + cur_start = 0; + } + return true; + } else { + sink.os << detail::serialize_multipart_formdata_finish(boundary); + sink.done(); + return true; + } + }; +} + +inline bool ClientImpl::process_socket( + const Socket &socket, + std::chrono::time_point start_time, + std::function callback) { + return detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback)); +} + +inline bool ClientImpl::is_ssl() const { return false; } + +inline Result ClientImpl::Get(const std::string &path, + DownloadProgress progress) { + return Get(path, Headers(), std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + DownloadProgress progress) { + if (params.empty()) { return Get(path, headers); } + + std::string path_with_query = append_query_params(path, params); + return Get(path_with_query, headers, std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + DownloadProgress progress) { + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.download_progress = std::move(progress); + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + return send_(std::move(req)); +} + +inline Result ClientImpl::Get(const std::string &path, + ContentReceiver content_receiver, + DownloadProgress progress) { + return Get(path, Headers(), nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver, + DownloadProgress progress) { + return Get(path, headers, nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, + ResponseHandler response_handler, + ContentReceiver content_receiver, + DownloadProgress progress) { + return Get(path, Headers(), std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + DownloadProgress progress) { + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.response_handler = std::move(response_handler); + req.content_receiver = + [content_receiver](const char *data, size_t data_length, + size_t /*offset*/, size_t /*total_length*/) { + return content_receiver(data, data_length); + }; + req.download_progress = std::move(progress); + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + return send_(std::move(req)); +} + +inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ContentReceiver content_receiver, + DownloadProgress progress) { + return Get(path, params, headers, nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + DownloadProgress progress) { + if (params.empty()) { + return Get(path, headers, std::move(response_handler), + std::move(content_receiver), std::move(progress)); + } + + std::string path_with_query = append_query_params(path, params); + return Get(path_with_query, headers, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} + +inline Result ClientImpl::Head(const std::string &path) { + return Head(path, Headers()); +} + +inline Result ClientImpl::Head(const std::string &path, + const Headers &headers) { + Request req; + req.method = "HEAD"; + req.headers = headers; + req.path = path; + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + return send_(std::move(req)); +} + +inline Result ClientImpl::Post(const std::string &path) { + return Post(path, std::string(), std::string()); +} + +inline Result ClientImpl::Post(const std::string &path, + const Headers &headers) { + return Post(path, headers, nullptr, 0, std::string()); +} + +inline Result ClientImpl::Post(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return Post(path, Headers(), body, content_length, content_type, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return Post(path, Headers(), body, content_type, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Params ¶ms) { + return Post(path, Headers(), params); +} + +inline Result ClientImpl::Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return Post(path, Headers(), content_length, std::move(content_provider), + content_type, progress); +} + +inline Result ClientImpl::Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return Post(path, Headers(), content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} + +inline Result ClientImpl::Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return Post(path, Headers(), std::move(content_provider), content_type, + progress); +} + +inline Result ClientImpl::Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return Post(path, Headers(), std::move(content_provider), content_type, + std::move(content_receiver), progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const Params ¶ms) { + auto query = detail::params_to_query_str(params); + return Post(path, headers, query, "application/x-www-form-urlencoded"); +} + +inline Result ClientImpl::Post(const std::string &path, + const UploadFormDataItems &items, + UploadProgress progress) { + return Post(path, Headers(), items, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + UploadProgress progress) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Post(path, headers, body, content_type, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const std::string &boundary, + UploadProgress progress) { + if (!detail::is_multipart_boundary_chars_valid(boundary)) { + return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; + } + + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Post(path, headers, body, content_type, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "POST", path, headers, body, content_length, nullptr, nullptr, + content_type, nullptr, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "POST", path, headers, body.data(), body.size(), nullptr, nullptr, + content_type, nullptr, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "POST", path, headers, nullptr, content_length, + std::move(content_provider), nullptr, content_type, nullptr, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return send_with_content_provider_and_receiver( + "POST", path, headers, nullptr, content_length, + std::move(content_provider), nullptr, content_type, + std::move(content_receiver), std::move(progress)); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), + content_type, nullptr, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return send_with_content_provider_and_receiver( + "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), + content_type, std::move(content_receiver), std::move(progress)); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const FormDataProviderItems &provider_items, + UploadProgress progress) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + return send_with_content_provider_and_receiver( + "POST", path, headers, nullptr, 0, nullptr, + get_multipart_content_provider(boundary, items, provider_items), + content_type, nullptr, progress); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + Request req; + req.method = "POST"; + req.path = path; + req.headers = headers; + req.body = body; + req.content_receiver = + [content_receiver](const char *data, size_t data_length, + size_t /*offset*/, size_t /*total_length*/) { + return content_receiver(data, data_length); + }; + req.download_progress = std::move(progress); + + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + + return send_(std::move(req)); +} + +inline Result ClientImpl::Put(const std::string &path) { + return Put(path, std::string(), std::string()); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers) { + return Put(path, headers, nullptr, 0, std::string()); +} + +inline Result ClientImpl::Put(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return Put(path, Headers(), body, content_length, content_type, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return Put(path, Headers(), body, content_type, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Params ¶ms) { + return Put(path, Headers(), params); +} + +inline Result ClientImpl::Put(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return Put(path, Headers(), content_length, std::move(content_provider), + content_type, progress); +} + +inline Result ClientImpl::Put(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return Put(path, Headers(), content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} + +inline Result ClientImpl::Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return Put(path, Headers(), std::move(content_provider), content_type, + progress); +} + +inline Result ClientImpl::Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return Put(path, Headers(), std::move(content_provider), content_type, + std::move(content_receiver), progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const Params ¶ms) { + auto query = detail::params_to_query_str(params); + return Put(path, headers, query, "application/x-www-form-urlencoded"); +} + +inline Result ClientImpl::Put(const std::string &path, + const UploadFormDataItems &items, + UploadProgress progress) { + return Put(path, Headers(), items, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + UploadProgress progress) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Put(path, headers, body, content_type, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const std::string &boundary, + UploadProgress progress) { + if (!detail::is_multipart_boundary_chars_valid(boundary)) { + return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; + } + + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Put(path, headers, body, content_type, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PUT", path, headers, body, content_length, nullptr, nullptr, + content_type, nullptr, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PUT", path, headers, body.data(), body.size(), nullptr, nullptr, + content_type, nullptr, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PUT", path, headers, nullptr, content_length, + std::move(content_provider), nullptr, content_type, nullptr, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PUT", path, headers, nullptr, content_length, + std::move(content_provider), nullptr, content_type, + std::move(content_receiver), progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), + content_type, nullptr, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const FormDataProviderItems &provider_items, + UploadProgress progress) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + return send_with_content_provider_and_receiver( + "PUT", path, headers, nullptr, 0, nullptr, + get_multipart_content_provider(boundary, items, provider_items), + content_type, nullptr, progress); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + Request req; + req.method = "PUT"; + req.path = path; + req.headers = headers; + req.body = body; + req.content_receiver = + [content_receiver](const char *data, size_t data_length, + size_t /*offset*/, size_t /*total_length*/) { + return content_receiver(data, data_length); + }; + req.download_progress = std::move(progress); + + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + + return send_(std::move(req)); +} + +inline Result ClientImpl::Patch(const std::string &path) { + return Patch(path, std::string(), std::string()); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + UploadProgress progress) { + return Patch(path, headers, nullptr, 0, std::string(), progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return Patch(path, Headers(), body, content_length, content_type, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return Patch(path, Headers(), body, content_type, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Params ¶ms) { + return Patch(path, Headers(), params); +} + +inline Result ClientImpl::Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return Patch(path, Headers(), content_length, std::move(content_provider), + content_type, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return Patch(path, Headers(), content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} + +inline Result ClientImpl::Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return Patch(path, Headers(), std::move(content_provider), content_type, + progress); +} + +inline Result ClientImpl::Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return Patch(path, Headers(), std::move(content_provider), content_type, + std::move(content_receiver), progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const Params ¶ms) { + auto query = detail::params_to_query_str(params); + return Patch(path, headers, query, "application/x-www-form-urlencoded"); +} + +inline Result ClientImpl::Patch(const std::string &path, + const UploadFormDataItems &items, + UploadProgress progress) { + return Patch(path, Headers(), items, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + UploadProgress progress) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Patch(path, headers, body, content_type, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const std::string &boundary, + UploadProgress progress) { + if (!detail::is_multipart_boundary_chars_valid(boundary)) { + return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; + } + + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Patch(path, headers, body, content_type, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PATCH", path, headers, body, content_length, nullptr, nullptr, + content_type, nullptr, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PATCH", path, headers, body.data(), body.size(), nullptr, nullptr, + content_type, nullptr, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PATCH", path, headers, nullptr, content_length, + std::move(content_provider), nullptr, content_type, nullptr, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PATCH", path, headers, nullptr, content_length, + std::move(content_provider), nullptr, content_type, + std::move(content_receiver), progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), + content_type, nullptr, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return send_with_content_provider_and_receiver( + "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const FormDataProviderItems &provider_items, + UploadProgress progress) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + return send_with_content_provider_and_receiver( + "PATCH", path, headers, nullptr, 0, nullptr, + get_multipart_content_provider(boundary, items, provider_items), + content_type, nullptr, progress); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + Request req; + req.method = "PATCH"; + req.path = path; + req.headers = headers; + req.body = body; + req.content_receiver = + [content_receiver](const char *data, size_t data_length, + size_t /*offset*/, size_t /*total_length*/) { + return content_receiver(data, data_length); + }; + req.download_progress = std::move(progress); + + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + + return send_(std::move(req)); +} + +inline Result ClientImpl::Delete(const std::string &path, + DownloadProgress progress) { + return Delete(path, Headers(), std::string(), std::string(), progress); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers, + DownloadProgress progress) { + return Delete(path, headers, std::string(), std::string(), progress); +} + +inline Result ClientImpl::Delete(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + DownloadProgress progress) { + return Delete(path, Headers(), body, content_length, content_type, progress); +} + +inline Result ClientImpl::Delete(const std::string &path, + const std::string &body, + const std::string &content_type, + DownloadProgress progress) { + return Delete(path, Headers(), body.data(), body.size(), content_type, + progress); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers, + const std::string &body, + const std::string &content_type, + DownloadProgress progress) { + return Delete(path, headers, body.data(), body.size(), content_type, + progress); +} + +inline Result ClientImpl::Delete(const std::string &path, const Params ¶ms, + DownloadProgress progress) { + return Delete(path, Headers(), params, progress); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers, const Params ¶ms, + DownloadProgress progress) { + auto query = detail::params_to_query_str(params); + return Delete(path, headers, query, "application/x-www-form-urlencoded", + progress); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers, const char *body, + size_t content_length, + const std::string &content_type, + DownloadProgress progress) { + Request req; + req.method = "DELETE"; + req.headers = headers; + req.path = path; + req.download_progress = std::move(progress); + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + req.body.assign(body, content_length); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Options(const std::string &path) { + return Options(path, Headers()); +} + +inline Result ClientImpl::Options(const std::string &path, + const Headers &headers) { + Request req; + req.method = "OPTIONS"; + req.headers = headers; + req.path = path; + if (max_timeout_msec_ > 0) { + req.start_time_ = std::chrono::steady_clock::now(); + } + + return send_(std::move(req)); +} + +inline void ClientImpl::stop() { + std::lock_guard guard(socket_mutex_); + + // If there is anything ongoing right now, the ONLY thread-safe thing we can + // do is to shutdown_socket, so that threads using this socket suddenly + // discover they can't read/write any more and error out. Everything else + // (closing the socket, shutting ssl down) is unsafe because these actions + // are not thread-safe. + if (socket_requests_in_flight_ > 0) { + shutdown_socket(socket_); + + // Aside from that, we set a flag for the socket to be closed when we're + // done. + socket_should_be_closed_when_request_is_done_ = true; + return; + } + + // Otherwise, still holding the mutex, we can shut everything down ourselves + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); +} + +inline std::string ClientImpl::host() const { return host_; } + +inline int ClientImpl::port() const { return port_; } + +inline size_t ClientImpl::is_socket_open() const { + std::lock_guard guard(socket_mutex_); + return socket_.is_open(); +} + +inline socket_t ClientImpl::socket() const { return socket_.sock; } + +inline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { + connection_timeout_sec_ = sec; + connection_timeout_usec_ = usec; +} + +inline void ClientImpl::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; +} + +inline void ClientImpl::set_write_timeout(time_t sec, time_t usec) { + write_timeout_sec_ = sec; + write_timeout_usec_ = usec; +} + +inline void ClientImpl::set_max_timeout(time_t msec) { + max_timeout_msec_ = msec; +} + +inline void ClientImpl::set_basic_auth(const std::string &username, + const std::string &password) { + basic_auth_username_ = username; + basic_auth_password_ = password; +} + +inline void ClientImpl::set_bearer_token_auth(const std::string &token) { + bearer_token_auth_token_ = token; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_digest_auth(const std::string &username, + const std::string &password) { + digest_auth_username_ = username; + digest_auth_password_ = password; +} +#endif + +inline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } + +inline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } + +inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; } + +inline void +ClientImpl::set_hostname_addr_map(std::map addr_map) { + addr_map_ = std::move(addr_map); +} + +inline void ClientImpl::set_default_headers(Headers headers) { + default_headers_ = std::move(headers); +} + +inline void ClientImpl::set_header_writer( + std::function const &writer) { + header_writer_ = writer; +} + +inline void ClientImpl::set_address_family(int family) { + address_family_ = family; +} + +inline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } + +inline void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; } + +inline void ClientImpl::set_socket_options(SocketOptions socket_options) { + socket_options_ = std::move(socket_options); +} + +inline void ClientImpl::set_compress(bool on) { compress_ = on; } + +inline void ClientImpl::set_decompress(bool on) { decompress_ = on; } + +inline void ClientImpl::set_interface(const std::string &intf) { + interface_ = intf; +} + +inline void ClientImpl::set_proxy(const std::string &host, int port) { + proxy_host_ = host; + proxy_port_ = port; +} + +inline void ClientImpl::set_proxy_basic_auth(const std::string &username, + const std::string &password) { + proxy_basic_auth_username_ = username; + proxy_basic_auth_password_ = password; +} + +inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) { + proxy_bearer_token_auth_token_ = token; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_proxy_digest_auth(const std::string &username, + const std::string &password) { + proxy_digest_auth_username_ = username; + proxy_digest_auth_password_ = password; +} + +inline void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path) { + ca_cert_file_path_ = ca_cert_file_path; + ca_cert_dir_path_ = ca_cert_dir_path; +} + +inline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store && ca_cert_store != ca_cert_store_) { + ca_cert_store_ = ca_cert_store; + } +} + +inline X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert, + std::size_t size) const { + auto mem = BIO_new_mem_buf(ca_cert, static_cast(size)); + auto se = detail::scope_exit([&] { BIO_free_all(mem); }); + if (!mem) { return nullptr; } + + auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); + if (!inf) { return nullptr; } + + auto cts = X509_STORE_new(); + if (cts) { + for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { + auto itmp = sk_X509_INFO_value(inf, i); + if (!itmp) { continue; } + + if (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); } + if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); } + } + } + + sk_X509_INFO_pop_free(inf, X509_INFO_free); + return cts; +} + +inline void ClientImpl::enable_server_certificate_verification(bool enabled) { + server_certificate_verification_ = enabled; +} + +inline void ClientImpl::enable_server_hostname_verification(bool enabled) { + server_hostname_verification_ = enabled; +} + +inline void ClientImpl::set_server_certificate_verifier( + std::function verifier) { + server_certificate_verifier_ = verifier; +} +#endif + +inline void ClientImpl::set_logger(Logger logger) { + logger_ = std::move(logger); +} + +inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { + error_logger_ = std::move(error_logger); +} + +/* + * SSL Implementation + */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace detail { + +inline bool is_ip_address(const std::string &host) { + struct in_addr addr4; + struct in6_addr addr6; + return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || + inet_pton(AF_INET6, host.c_str(), &addr6) == 1; +} + +template +inline SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex, + U SSL_connect_or_accept, V setup) { + SSL *ssl = nullptr; + { + std::lock_guard guard(ctx_mutex); + ssl = SSL_new(ctx); + } + + if (ssl) { + set_nonblocking(sock, true); + auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); + BIO_set_nbio(bio, 1); + SSL_set_bio(ssl, bio, bio); + + if (!setup(ssl) || SSL_connect_or_accept(ssl) != 1) { + SSL_shutdown(ssl); + { + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); + } + set_nonblocking(sock, false); + return nullptr; + } + BIO_set_nbio(bio, 0); + set_nonblocking(sock, false); + } + + return ssl; +} + +inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock, + bool shutdown_gracefully) { + // sometimes we may want to skip this to try to avoid SIGPIPE if we know + // the remote has closed the network connection + // Note that it is not always possible to avoid SIGPIPE, this is merely a + // best-efforts. + if (shutdown_gracefully) { + (void)(sock); + // SSL_shutdown() returns 0 on first call (indicating close_notify alert + // sent) and 1 on subsequent call (indicating close_notify alert received) + if (SSL_shutdown(ssl) == 0) { + // Expected to return 1, but even if it doesn't, we free ssl + SSL_shutdown(ssl); + } + } + + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); +} + +template +bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl, + U ssl_connect_or_accept, + time_t timeout_sec, time_t timeout_usec, + int *ssl_error) { + auto res = 0; + while ((res = ssl_connect_or_accept(ssl)) != 1) { + auto err = SSL_get_error(ssl, res); + switch (err) { + case SSL_ERROR_WANT_READ: + if (select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } + break; + case SSL_ERROR_WANT_WRITE: + if (select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } + break; + default: break; + } + if (ssl_error) { *ssl_error = err; } + return false; + } + return true; +} + +template +inline bool process_server_socket_ssl( + const std::atomic &svr_sock, SSL *ssl, socket_t sock, + size_t keep_alive_max_count, time_t keep_alive_timeout_sec, + time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, T callback) { + return process_server_socket_core( + svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, + [&](bool close_connection, bool &connection_closed) { + SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm, close_connection, connection_closed); + }); +} + +template +inline bool process_client_socket_ssl( + SSL *ssl, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + time_t max_timeout_msec, + std::chrono::time_point start_time, T callback) { + SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec, max_timeout_msec, + start_time); + return callback(strm); +} + +// SSL socket stream implementation +inline SSLSocketStream::SSLSocketStream( + socket_t sock, SSL *ssl, time_t read_timeout_sec, time_t read_timeout_usec, + time_t write_timeout_sec, time_t write_timeout_usec, + time_t max_timeout_msec, + std::chrono::time_point start_time) + : sock_(sock), ssl_(ssl), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec), + max_timeout_msec_(max_timeout_msec), start_time_(start_time) { + SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); +} + +inline SSLSocketStream::~SSLSocketStream() = default; + +inline bool SSLSocketStream::is_readable() const { + return SSL_pending(ssl_) > 0; +} + +inline bool SSLSocketStream::wait_readable() const { + if (max_timeout_msec_ <= 0) { + return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; + } + + time_t read_timeout_sec; + time_t read_timeout_usec; + calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, + read_timeout_usec_, read_timeout_sec, read_timeout_usec); + + return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; +} + +inline bool SSLSocketStream::wait_writable() const { + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && + is_socket_alive(sock_) && !is_ssl_peer_could_be_closed(ssl_, sock_); +} + +inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { + if (SSL_pending(ssl_) > 0) { + return SSL_read(ssl_, ptr, static_cast(size)); + } else if (wait_readable()) { + auto ret = SSL_read(ssl_, ptr, static_cast(size)); + if (ret < 0) { + auto err = SSL_get_error(ssl_, ret); + auto n = 1000; +#ifdef _WIN32 + while (--n >= 0 && (err == SSL_ERROR_WANT_READ || + (err == SSL_ERROR_SYSCALL && + WSAGetLastError() == WSAETIMEDOUT))) { +#else + while (--n >= 0 && err == SSL_ERROR_WANT_READ) { +#endif + if (SSL_pending(ssl_) > 0) { + return SSL_read(ssl_, ptr, static_cast(size)); + } else if (wait_readable()) { + std::this_thread::sleep_for(std::chrono::microseconds{10}); + ret = SSL_read(ssl_, ptr, static_cast(size)); + if (ret >= 0) { return ret; } + err = SSL_get_error(ssl_, ret); + } else { + break; + } + } + assert(ret < 0); + } + return ret; + } else { + return -1; + } +} + +inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { + if (wait_writable()) { + auto handle_size = static_cast( + std::min(size, (std::numeric_limits::max)())); + + auto ret = SSL_write(ssl_, ptr, static_cast(handle_size)); + if (ret < 0) { + auto err = SSL_get_error(ssl_, ret); + auto n = 1000; +#ifdef _WIN32 + while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE || + (err == SSL_ERROR_SYSCALL && + WSAGetLastError() == WSAETIMEDOUT))) { +#else + while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) { +#endif + if (wait_writable()) { + std::this_thread::sleep_for(std::chrono::microseconds{10}); + ret = SSL_write(ssl_, ptr, static_cast(handle_size)); + if (ret >= 0) { return ret; } + err = SSL_get_error(ssl_, ret); + } else { + break; + } + } + assert(ret < 0); + } + return ret; + } + return -1; +} + +inline void SSLSocketStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + detail::get_remote_ip_and_port(sock_, ip, port); +} + +inline void SSLSocketStream::get_local_ip_and_port(std::string &ip, + int &port) const { + detail::get_local_ip_and_port(sock_, ip, port); +} + +inline socket_t SSLSocketStream::socket() const { return sock_; } + +inline time_t SSLSocketStream::duration() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time_) + .count(); +} + +} // namespace detail + +// SSL HTTP server implementation +inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path, + const char *client_ca_cert_file_path, + const char *client_ca_cert_dir_path, + const char *private_key_password) { + ctx_ = SSL_CTX_new(TLS_server_method()); + + if (ctx_) { + SSL_CTX_set_options(ctx_, + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION); + + if (private_key_password != nullptr && (private_key_password[0] != '\0')) { + SSL_CTX_set_default_passwd_cb_userdata( + ctx_, + reinterpret_cast(const_cast(private_key_password))); + } + + if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_check_private_key(ctx_) != 1) { + last_ssl_error_ = static_cast(ERR_get_error()); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } else if (client_ca_cert_file_path || client_ca_cert_dir_path) { + SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path, + client_ca_cert_dir_path); + + // Set client CA list to be sent to clients during TLS handshake + if (client_ca_cert_file_path) { + auto ca_list = SSL_load_client_CA_file(client_ca_cert_file_path); + if (ca_list != nullptr) { + SSL_CTX_set_client_CA_list(ctx_, ca_list); + } else { + // Failed to load client CA list, but we continue since + // SSL_CTX_load_verify_locations already succeeded and + // certificate verification will still work + last_ssl_error_ = static_cast(ERR_get_error()); + } + } + + SSL_CTX_set_verify( + ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + } + } +} + +inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store) { + ctx_ = SSL_CTX_new(TLS_server_method()); + + if (ctx_) { + SSL_CTX_set_options(ctx_, + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION); + + if (SSL_CTX_use_certificate(ctx_, cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, private_key) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } else if (client_ca_cert_store) { + SSL_CTX_set_cert_store(ctx_, client_ca_cert_store); + + // Extract CA names from the store and set them as the client CA list + auto ca_list = extract_ca_names_from_x509_store(client_ca_cert_store); + if (ca_list) { + SSL_CTX_set_client_CA_list(ctx_, ca_list); + } else { + // Failed to extract CA names, record the error + last_ssl_error_ = static_cast(ERR_get_error()); + } + + SSL_CTX_set_verify( + ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + } + } +} + +inline SSLServer::SSLServer( + const std::function &setup_ssl_ctx_callback) { + ctx_ = SSL_CTX_new(TLS_method()); + if (ctx_) { + if (!setup_ssl_ctx_callback(*ctx_)) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLServer::~SSLServer() { + if (ctx_) { SSL_CTX_free(ctx_); } +} + +inline bool SSLServer::is_valid() const { return ctx_; } + +inline SSL_CTX *SSLServer::ssl_context() const { return ctx_; } + +inline void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store) { + + std::lock_guard guard(ctx_mutex_); + + SSL_CTX_use_certificate(ctx_, cert); + SSL_CTX_use_PrivateKey(ctx_, private_key); + + if (client_ca_cert_store != nullptr) { + SSL_CTX_set_cert_store(ctx_, client_ca_cert_store); + } +} + +inline bool SSLServer::process_and_close_socket(socket_t sock) { + auto ssl = detail::ssl_new( + sock, ctx_, ctx_mutex_, + [&](SSL *ssl2) { + return detail::ssl_connect_or_accept_nonblocking( + sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_, + &last_ssl_error_); + }, + [](SSL * /*ssl2*/) { return true; }); + + auto ret = false; + if (ssl) { + std::string remote_addr; + int remote_port = 0; + detail::get_remote_ip_and_port(sock, remote_addr, remote_port); + + std::string local_addr; + int local_port = 0; + detail::get_local_ip_and_port(sock, local_addr, local_port); + + ret = detail::process_server_socket_ssl( + svr_sock_, ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, remote_addr, remote_port, local_addr, + local_port, close_connection, + connection_closed, + [&](Request &req) { req.ssl = ssl; }); + }); + + // Shutdown gracefully if the result seemed successful, non-gracefully if + // the connection appeared to be closed. + const bool shutdown_gracefully = ret; + detail::ssl_delete(ctx_mutex_, ssl, sock, shutdown_gracefully); + } + + detail::shutdown_socket(sock); + detail::close_socket(sock); + return ret; +} + +inline STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store( + X509_STORE *store) { + if (!store) { return nullptr; } + + auto ca_list = sk_X509_NAME_new_null(); + if (!ca_list) { return nullptr; } + + // Get all objects from the store + auto objs = X509_STORE_get0_objects(store); + if (!objs) { + sk_X509_NAME_free(ca_list); + return nullptr; + } + + // Iterate through objects and extract certificate subject names + for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) { + auto obj = sk_X509_OBJECT_value(objs, i); + if (X509_OBJECT_get_type(obj) == X509_LU_X509) { + auto cert = X509_OBJECT_get0_X509(obj); + if (cert) { + auto subject = X509_get_subject_name(cert); + if (subject) { + auto name_dup = X509_NAME_dup(subject); + if (name_dup) { sk_X509_NAME_push(ca_list, name_dup); } + } + } + } + } + + // If no names were extracted, free the list and return nullptr + if (sk_X509_NAME_num(ca_list) == 0) { + sk_X509_NAME_free(ca_list); + return nullptr; + } + + return ca_list; +} + +// SSL HTTP client implementation +inline SSLClient::SSLClient(const std::string &host) + : SSLClient(host, 443, std::string(), std::string()) {} + +inline SSLClient::SSLClient(const std::string &host, int port) + : SSLClient(host, port, std::string(), std::string()) {} + +inline SSLClient::SSLClient(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path, + const std::string &private_key_password) + : ClientImpl(host, port, client_cert_path, client_key_path) { + ctx_ = SSL_CTX_new(TLS_client_method()); + + SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION); + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(b, e); + }); + + if (!client_cert_path.empty() && !client_key_path.empty()) { + if (!private_key_password.empty()) { + SSL_CTX_set_default_passwd_cb_userdata( + ctx_, reinterpret_cast( + const_cast(private_key_password.c_str()))); + } + + if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(), + SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(), + SSL_FILETYPE_PEM) != 1) { + last_openssl_error_ = ERR_get_error(); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::SSLClient(const std::string &host, int port, + X509 *client_cert, EVP_PKEY *client_key, + const std::string &private_key_password) + : ClientImpl(host, port) { + ctx_ = SSL_CTX_new(TLS_client_method()); + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(b, e); + }); + + if (client_cert != nullptr && client_key != nullptr) { + if (!private_key_password.empty()) { + SSL_CTX_set_default_passwd_cb_userdata( + ctx_, reinterpret_cast( + const_cast(private_key_password.c_str()))); + } + + if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) { + last_openssl_error_ = ERR_get_error(); + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::~SSLClient() { + if (ctx_) { SSL_CTX_free(ctx_); } + // Make sure to shut down SSL since shutdown_ssl will resolve to the + // base function rather than the derived function once we get to the + // base class destructor, and won't free the SSL (causing a leak). + shutdown_ssl_impl(socket_, true); +} + +inline bool SSLClient::is_valid() const { return ctx_; } + +inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store) { + if (ctx_) { + if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) { + // Free memory allocated for old cert and use new store + // `ca_cert_store` + SSL_CTX_set_cert_store(ctx_, ca_cert_store); + ca_cert_store_ = ca_cert_store; + } + } else { + X509_STORE_free(ca_cert_store); + } + } +} + +inline void SSLClient::load_ca_cert_store(const char *ca_cert, + std::size_t size) { + set_ca_cert_store(ClientImpl::create_ca_cert_store(ca_cert, size)); +} + +inline long SSLClient::get_openssl_verify_result() const { + return verify_result_; +} + +inline SSL_CTX *SSLClient::ssl_context() const { return ctx_; } + +inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { + if (!is_valid()) { + error = Error::SSLConnection; + return false; + } + return ClientImpl::create_and_connect_socket(socket, error); +} + +// Assumes that socket_mutex_ is locked and that there are no requests in +// flight +inline bool SSLClient::connect_with_proxy( + Socket &socket, + std::chrono::time_point start_time, + Response &res, bool &success, Error &error) { + success = true; + Response proxy_res; + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, + start_time, [&](Stream &strm) { + Request req2; + req2.method = "CONNECT"; + req2.path = host_and_port_; + if (max_timeout_msec_ > 0) { + req2.start_time_ = std::chrono::steady_clock::now(); + } + return process_request(strm, req2, proxy_res, false, error); + })) { + // Thread-safe to close everything because we are assuming there are no + // requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + success = false; + return false; + } + + if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) { + if (!proxy_digest_auth_username_.empty() && + !proxy_digest_auth_password_.empty()) { + std::map auth; + if (detail::parse_www_authenticate(proxy_res, auth, true)) { + // Close the current socket and create a new one for the authenticated + // request + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + + // Create a new socket for the authenticated CONNECT request + if (!ensure_socket_connection(socket, error)) { + success = false; + output_error_log(error, nullptr); + return false; + } + + proxy_res = Response(); + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, + start_time, [&](Stream &strm) { + Request req3; + req3.method = "CONNECT"; + req3.path = host_and_port_; + req3.headers.insert(detail::make_digest_authentication_header( + req3, auth, 1, detail::random_string(10), + proxy_digest_auth_username_, proxy_digest_auth_password_, + true)); + if (max_timeout_msec_ > 0) { + req3.start_time_ = std::chrono::steady_clock::now(); + } + return process_request(strm, req3, proxy_res, false, error); + })) { + // Thread-safe to close everything because we are assuming there are + // no requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + success = false; + return false; + } + } + } + } + + // If status code is not 200, proxy request is failed. + // Set error to ProxyConnection and return proxy response + // as the response of the request + if (proxy_res.status != StatusCode::OK_200) { + error = Error::ProxyConnection; + output_error_log(error, nullptr); + res = std::move(proxy_res); + // Thread-safe to close everything because we are assuming there are + // no requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + return false; + } + + return true; +} + +inline bool SSLClient::load_certs() { + auto ret = true; + + std::call_once(initialize_cert_, [&]() { + std::lock_guard guard(ctx_mutex_); + if (!ca_cert_file_path_.empty()) { + if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(), + nullptr)) { + last_openssl_error_ = ERR_get_error(); + ret = false; + } + } else if (!ca_cert_dir_path_.empty()) { + if (!SSL_CTX_load_verify_locations(ctx_, nullptr, + ca_cert_dir_path_.c_str())) { + last_openssl_error_ = ERR_get_error(); + ret = false; + } + } else { + auto loaded = false; +#ifdef _WIN32 + loaded = + detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && TARGET_OS_MAC + loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); +#endif // _WIN32 + if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } + } + }); + + return ret; +} + +inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { + auto ssl = detail::ssl_new( + socket.sock, ctx_, ctx_mutex_, + [&](SSL *ssl2) { + if (server_certificate_verification_) { + if (!load_certs()) { + error = Error::SSLLoadingCerts; + output_error_log(error, nullptr); + return false; + } + SSL_set_verify(ssl2, SSL_VERIFY_NONE, nullptr); + } + + if (!detail::ssl_connect_or_accept_nonblocking( + socket.sock, ssl2, SSL_connect, connection_timeout_sec_, + connection_timeout_usec_, &last_ssl_error_)) { + error = Error::SSLConnection; + output_error_log(error, nullptr); + return false; + } + + if (server_certificate_verification_) { + auto verification_status = SSLVerifierResponse::NoDecisionMade; + + if (server_certificate_verifier_) { + verification_status = server_certificate_verifier_(ssl2); + } + + if (verification_status == SSLVerifierResponse::CertificateRejected) { + last_openssl_error_ = ERR_get_error(); + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + return false; + } + + if (verification_status == SSLVerifierResponse::NoDecisionMade) { + verify_result_ = SSL_get_verify_result(ssl2); + + if (verify_result_ != X509_V_OK) { + last_openssl_error_ = static_cast(verify_result_); + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + return false; + } + + auto server_cert = SSL_get1_peer_certificate(ssl2); + auto se = detail::scope_exit([&] { X509_free(server_cert); }); + + if (server_cert == nullptr) { + last_openssl_error_ = ERR_get_error(); + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + return false; + } + + if (server_hostname_verification_) { + if (!verify_host(server_cert)) { + last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH; + error = Error::SSLServerHostnameVerification; + output_error_log(error, nullptr); + return false; + } + } + } + } + + return true; + }, + [&](SSL *ssl2) { + // Set SNI only if host is not IP address + if (!detail::is_ip_address(host_)) { +#if defined(OPENSSL_IS_BORINGSSL) + SSL_set_tlsext_host_name(ssl2, host_.c_str()); +#else + // NOTE: Direct call instead of using the OpenSSL macro to suppress + // -Wold-style-cast warning + SSL_ctrl(ssl2, SSL_CTRL_SET_TLSEXT_HOSTNAME, + TLSEXT_NAMETYPE_host_name, + static_cast(const_cast(host_.c_str()))); +#endif + } + return true; + }); + + if (ssl) { + socket.ssl = ssl; + return true; + } + + if (ctx_ == nullptr) { + error = Error::SSLConnection; + last_openssl_error_ = ERR_get_error(); + } + + shutdown_socket(socket); + close_socket(socket); + return false; +} + +inline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { + shutdown_ssl_impl(socket, shutdown_gracefully); +} + +inline void SSLClient::shutdown_ssl_impl(Socket &socket, + bool shutdown_gracefully) { + if (socket.sock == INVALID_SOCKET) { + assert(socket.ssl == nullptr); + return; + } + if (socket.ssl) { + detail::ssl_delete(ctx_mutex_, socket.ssl, socket.sock, + shutdown_gracefully); + socket.ssl = nullptr; + } + assert(socket.ssl == nullptr); +} + +inline bool SSLClient::process_socket( + const Socket &socket, + std::chrono::time_point start_time, + std::function callback) { + assert(socket.ssl); + return detail::process_client_socket_ssl( + socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, + std::move(callback)); +} + +inline bool SSLClient::is_ssl() const { return true; } + +inline bool SSLClient::verify_host(X509 *server_cert) const { + /* Quote from RFC2818 section 3.1 "Server Identity" + + If a subjectAltName extension of type dNSName is present, that MUST + be used as the identity. Otherwise, the (most specific) Common Name + field in the Subject field of the certificate MUST be used. Although + the use of the Common Name is existing practice, it is deprecated and + Certification Authorities are encouraged to use the dNSName instead. + + Matching is performed using the matching rules specified by + [RFC2459]. If more than one identity of a given type is present in + the certificate (e.g., more than one dNSName name, a match in any one + of the set is considered acceptable.) Names may contain the wildcard + character * which is considered to match any single domain name + component or component fragment. E.g., *.a.com matches foo.a.com but + not bar.foo.a.com. f*.com matches foo.com but not bar.com. + + In some cases, the URI is specified as an IP address rather than a + hostname. In this case, the iPAddress subjectAltName must be present + in the certificate and must exactly match the IP in the URI. + + */ + return verify_host_with_subject_alt_name(server_cert) || + verify_host_with_common_name(server_cert); +} + +inline bool +SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { + auto ret = false; + + auto type = GEN_DNS; + + struct in6_addr addr6 = {}; + struct in_addr addr = {}; + size_t addr_len = 0; + +#ifndef __MINGW32__ + if (inet_pton(AF_INET6, host_.c_str(), &addr6)) { + type = GEN_IPADD; + addr_len = sizeof(struct in6_addr); + } else if (inet_pton(AF_INET, host_.c_str(), &addr)) { + type = GEN_IPADD; + addr_len = sizeof(struct in_addr); + } +#endif + + auto alt_names = static_cast( + X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr)); + + if (alt_names) { + auto dsn_matched = false; + auto ip_matched = false; + + auto count = sk_GENERAL_NAME_num(alt_names); + + for (decltype(count) i = 0; i < count && !dsn_matched; i++) { + auto val = sk_GENERAL_NAME_value(alt_names, i); + if (!val || val->type != type) { continue; } + + auto name = + reinterpret_cast(ASN1_STRING_get0_data(val->d.ia5)); + if (name == nullptr) { continue; } + + auto name_len = static_cast(ASN1_STRING_length(val->d.ia5)); + + switch (type) { + case GEN_DNS: dsn_matched = check_host_name(name, name_len); break; + + case GEN_IPADD: + if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) { + ip_matched = true; + } + break; + } + } + + if (dsn_matched || ip_matched) { ret = true; } + } + + GENERAL_NAMES_free(const_cast( + reinterpret_cast(alt_names))); + return ret; +} + +inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const { + const auto subject_name = X509_get_subject_name(server_cert); + + if (subject_name != nullptr) { + char name[BUFSIZ]; + auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, + name, sizeof(name)); + + if (name_len != -1) { + return check_host_name(name, static_cast(name_len)); + } + } + + return false; +} + +inline bool SSLClient::check_host_name(const char *pattern, + size_t pattern_len) const { + if (host_.size() == pattern_len && host_ == pattern) { return true; } + + // Wildcard match + // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 + std::vector pattern_components; + detail::split(&pattern[0], &pattern[pattern_len], '.', + [&](const char *b, const char *e) { + pattern_components.emplace_back(b, e); + }); + + if (host_components_.size() != pattern_components.size()) { return false; } + + auto itr = pattern_components.begin(); + for (const auto &h : host_components_) { + auto &p = *itr; + if (p != h && p != "*") { + auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' && + !p.compare(0, p.size() - 1, h)); + if (!partial_match) { return false; } + } + ++itr; + } + + return true; +} +#endif + +// Universal client implementation +inline Client::Client(const std::string &scheme_host_port) + : Client(scheme_host_port, std::string(), std::string()) {} + +inline Client::Client(const std::string &scheme_host_port, + const std::string &client_cert_path, + const std::string &client_key_path) { + const static std::regex re( + R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)"); + + std::smatch m; + if (std::regex_match(scheme_host_port, m, re)) { + auto scheme = m[1].str(); + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (!scheme.empty() && (scheme != "http" && scheme != "https")) { +#else + if (!scheme.empty() && scheme != "http") { +#endif +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + std::string msg = "'" + scheme + "' scheme is not supported."; + throw std::invalid_argument(msg); +#endif + return; + } + + auto is_ssl = scheme == "https"; + + auto host = m[2].str(); + if (host.empty()) { host = m[3].str(); } + + auto port_str = m[4].str(); + auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80); + + if (is_ssl) { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + cli_ = detail::make_unique(host, port, client_cert_path, + client_key_path); + is_ssl_ = is_ssl; +#endif + } else { + cli_ = detail::make_unique(host, port, client_cert_path, + client_key_path); + } + } else { + // NOTE: Update TEST(UniversalClientImplTest, Ipv6LiteralAddress) + // if port param below changes. + cli_ = detail::make_unique(scheme_host_port, 80, + client_cert_path, client_key_path); + } +} // namespace detail + +inline Client::Client(const std::string &host, int port) + : cli_(detail::make_unique(host, port)) {} + +inline Client::Client(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : cli_(detail::make_unique(host, port, client_cert_path, + client_key_path)) {} + +inline Client::~Client() = default; + +inline bool Client::is_valid() const { + return cli_ != nullptr && cli_->is_valid(); +} + +inline Result Client::Get(const std::string &path, DownloadProgress progress) { + return cli_->Get(path, std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + DownloadProgress progress) { + return cli_->Get(path, headers, std::move(progress)); +} +inline Result Client::Get(const std::string &path, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Get(path, std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Get(path, headers, std::move(content_receiver), + std::move(progress)); +} +inline Result Client::Get(const std::string &path, + ResponseHandler response_handler, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Get(path, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Get(path, headers, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Params ¶ms, + const Headers &headers, DownloadProgress progress) { + return cli_->Get(path, params, headers, std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Get(path, params, headers, std::move(content_receiver), + std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Get(path, params, headers, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} + +inline Result Client::Head(const std::string &path) { return cli_->Head(path); } +inline Result Client::Head(const std::string &path, const Headers &headers) { + return cli_->Head(path, headers); +} + +inline Result Client::Post(const std::string &path) { return cli_->Post(path); } +inline Result Client::Post(const std::string &path, const Headers &headers) { + return cli_->Post(path, headers); +} +inline Result Client::Post(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, body, content_length, content_type, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, headers, body, content_length, content_type, + progress); +} +inline Result Client::Post(const std::string &path, const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, body, content_type, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, headers, body, content_type, progress); +} +inline Result Client::Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, content_length, std::move(content_provider), + content_type, progress); +} +inline Result Client::Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Post(path, content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} +inline Result Client::Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, std::move(content_provider), content_type, progress); +} +inline Result Client::Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Post(path, std::move(content_provider), content_type, + std::move(content_receiver), progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, headers, content_length, std::move(content_provider), + content_type, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Post(path, headers, content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Post(path, headers, std::move(content_provider), content_type, + progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Post(path, headers, std::move(content_provider), content_type, + std::move(content_receiver), progress); +} +inline Result Client::Post(const std::string &path, const Params ¶ms) { + return cli_->Post(path, params); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const Params ¶ms) { + return cli_->Post(path, headers, params); +} +inline Result Client::Post(const std::string &path, + const UploadFormDataItems &items, + UploadProgress progress) { + return cli_->Post(path, items, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + UploadProgress progress) { + return cli_->Post(path, headers, items, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const std::string &boundary, + UploadProgress progress) { + return cli_->Post(path, headers, items, boundary, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const FormDataProviderItems &provider_items, + UploadProgress progress) { + return cli_->Post(path, headers, items, provider_items, progress); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Post(path, headers, body, content_type, + std::move(content_receiver), progress); +} + +inline Result Client::Put(const std::string &path) { return cli_->Put(path); } +inline Result Client::Put(const std::string &path, const Headers &headers) { + return cli_->Put(path, headers); +} +inline Result Client::Put(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, body, content_length, content_type, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, headers, body, content_length, content_type, progress); +} +inline Result Client::Put(const std::string &path, const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, body, content_type, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, headers, body, content_type, progress); +} +inline Result Client::Put(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, content_length, std::move(content_provider), + content_type, progress); +} +inline Result Client::Put(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Put(path, content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} +inline Result Client::Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, std::move(content_provider), content_type, progress); +} +inline Result Client::Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Put(path, std::move(content_provider), content_type, + std::move(content_receiver), progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, headers, content_length, std::move(content_provider), + content_type, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Put(path, headers, content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Put(path, headers, std::move(content_provider), content_type, + progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Put(path, headers, std::move(content_provider), content_type, + std::move(content_receiver), progress); +} +inline Result Client::Put(const std::string &path, const Params ¶ms) { + return cli_->Put(path, params); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const Params ¶ms) { + return cli_->Put(path, headers, params); +} +inline Result Client::Put(const std::string &path, + const UploadFormDataItems &items, + UploadProgress progress) { + return cli_->Put(path, items, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + UploadProgress progress) { + return cli_->Put(path, headers, items, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const std::string &boundary, + UploadProgress progress) { + return cli_->Put(path, headers, items, boundary, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const FormDataProviderItems &provider_items, + UploadProgress progress) { + return cli_->Put(path, headers, items, provider_items, progress); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Put(path, headers, body, content_type, content_receiver, + progress); +} + +inline Result Client::Patch(const std::string &path) { + return cli_->Patch(path); +} +inline Result Client::Patch(const std::string &path, const Headers &headers) { + return cli_->Patch(path, headers); +} +inline Result Client::Patch(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, body, content_length, content_type, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, headers, body, content_length, content_type, + progress); +} +inline Result Client::Patch(const std::string &path, const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, body, content_type, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, headers, body, content_type, progress); +} +inline Result Client::Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, content_length, std::move(content_provider), + content_type, progress); +} +inline Result Client::Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Patch(path, content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} +inline Result Client::Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, std::move(content_provider), content_type, progress); +} +inline Result Client::Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Patch(path, std::move(content_provider), content_type, + std::move(content_receiver), progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, headers, content_length, std::move(content_provider), + content_type, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Patch(path, headers, content_length, std::move(content_provider), + content_type, std::move(content_receiver), progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + UploadProgress progress) { + return cli_->Patch(path, headers, std::move(content_provider), content_type, + progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type, + ContentReceiver content_receiver, + UploadProgress progress) { + return cli_->Patch(path, headers, std::move(content_provider), content_type, + std::move(content_receiver), progress); +} +inline Result Client::Patch(const std::string &path, const Params ¶ms) { + return cli_->Patch(path, params); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const Params ¶ms) { + return cli_->Patch(path, headers, params); +} +inline Result Client::Patch(const std::string &path, + const UploadFormDataItems &items, + UploadProgress progress) { + return cli_->Patch(path, items, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + UploadProgress progress) { + return cli_->Patch(path, headers, items, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const std::string &boundary, + UploadProgress progress) { + return cli_->Patch(path, headers, items, boundary, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const UploadFormDataItems &items, + const FormDataProviderItems &provider_items, + UploadProgress progress) { + return cli_->Patch(path, headers, items, provider_items, progress); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + ContentReceiver content_receiver, + DownloadProgress progress) { + return cli_->Patch(path, headers, body, content_type, content_receiver, + progress); +} + +inline Result Client::Delete(const std::string &path, + DownloadProgress progress) { + return cli_->Delete(path, progress); +} +inline Result Client::Delete(const std::string &path, const Headers &headers, + DownloadProgress progress) { + return cli_->Delete(path, headers, progress); +} +inline Result Client::Delete(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type, + DownloadProgress progress) { + return cli_->Delete(path, body, content_length, content_type, progress); +} +inline Result Client::Delete(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type, + DownloadProgress progress) { + return cli_->Delete(path, headers, body, content_length, content_type, + progress); +} +inline Result Client::Delete(const std::string &path, const std::string &body, + const std::string &content_type, + DownloadProgress progress) { + return cli_->Delete(path, body, content_type, progress); +} +inline Result Client::Delete(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type, + DownloadProgress progress) { + return cli_->Delete(path, headers, body, content_type, progress); +} +inline Result Client::Delete(const std::string &path, const Params ¶ms, + DownloadProgress progress) { + return cli_->Delete(path, params, progress); +} +inline Result Client::Delete(const std::string &path, const Headers &headers, + const Params ¶ms, DownloadProgress progress) { + return cli_->Delete(path, headers, params, progress); +} + +inline Result Client::Options(const std::string &path) { + return cli_->Options(path); +} +inline Result Client::Options(const std::string &path, const Headers &headers) { + return cli_->Options(path, headers); +} + +inline ClientImpl::StreamHandle +Client::open_stream(const std::string &method, const std::string &path, + const Params ¶ms, const Headers &headers, + const std::string &body, const std::string &content_type) { + return cli_->open_stream(method, path, params, headers, body, content_type); +} + +inline bool Client::send(Request &req, Response &res, Error &error) { + return cli_->send(req, res, error); +} + +inline Result Client::send(const Request &req) { return cli_->send(req); } + +inline void Client::stop() { cli_->stop(); } + +inline std::string Client::host() const { return cli_->host(); } + +inline int Client::port() const { return cli_->port(); } + +inline size_t Client::is_socket_open() const { return cli_->is_socket_open(); } + +inline socket_t Client::socket() const { return cli_->socket(); } + +inline void +Client::set_hostname_addr_map(std::map addr_map) { + cli_->set_hostname_addr_map(std::move(addr_map)); +} + +inline void Client::set_default_headers(Headers headers) { + cli_->set_default_headers(std::move(headers)); +} + +inline void Client::set_header_writer( + std::function const &writer) { + cli_->set_header_writer(writer); +} + +inline void Client::set_address_family(int family) { + cli_->set_address_family(family); +} + +inline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } + +inline void Client::set_socket_options(SocketOptions socket_options) { + cli_->set_socket_options(std::move(socket_options)); +} + +inline void Client::set_connection_timeout(time_t sec, time_t usec) { + cli_->set_connection_timeout(sec, usec); +} + +inline void Client::set_read_timeout(time_t sec, time_t usec) { + cli_->set_read_timeout(sec, usec); +} + +inline void Client::set_write_timeout(time_t sec, time_t usec) { + cli_->set_write_timeout(sec, usec); +} + +inline void Client::set_basic_auth(const std::string &username, + const std::string &password) { + cli_->set_basic_auth(username, password); +} +inline void Client::set_bearer_token_auth(const std::string &token) { + cli_->set_bearer_token_auth(token); +} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_digest_auth(const std::string &username, + const std::string &password) { + cli_->set_digest_auth(username, password); +} +#endif + +inline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } +inline void Client::set_follow_location(bool on) { + cli_->set_follow_location(on); +} + +inline void Client::set_path_encode(bool on) { cli_->set_path_encode(on); } + +[[deprecated("Use set_path_encode instead")]] +inline void Client::set_url_encode(bool on) { + cli_->set_path_encode(on); +} + +inline void Client::set_compress(bool on) { cli_->set_compress(on); } + +inline void Client::set_decompress(bool on) { cli_->set_decompress(on); } + +inline void Client::set_interface(const std::string &intf) { + cli_->set_interface(intf); +} + +inline void Client::set_proxy(const std::string &host, int port) { + cli_->set_proxy(host, port); +} +inline void Client::set_proxy_basic_auth(const std::string &username, + const std::string &password) { + cli_->set_proxy_basic_auth(username, password); +} +inline void Client::set_proxy_bearer_token_auth(const std::string &token) { + cli_->set_proxy_bearer_token_auth(token); +} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_proxy_digest_auth(const std::string &username, + const std::string &password) { + cli_->set_proxy_digest_auth(username, password); +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::enable_server_certificate_verification(bool enabled) { + cli_->enable_server_certificate_verification(enabled); +} + +inline void Client::enable_server_hostname_verification(bool enabled) { + cli_->enable_server_hostname_verification(enabled); +} + +inline void Client::set_server_certificate_verifier( + std::function verifier) { + cli_->set_server_certificate_verifier(verifier); +} +#endif + +inline void Client::set_logger(Logger logger) { + cli_->set_logger(std::move(logger)); +} + +inline void Client::set_error_logger(ErrorLogger error_logger) { + cli_->set_error_logger(std::move(error_logger)); +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path) { + cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path); +} + +inline void Client::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (is_ssl_) { + static_cast(*cli_).set_ca_cert_store(ca_cert_store); + } else { + cli_->set_ca_cert_store(ca_cert_store); + } +} + +inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { + set_ca_cert_store(cli_->create_ca_cert_store(ca_cert, size)); +} + +inline long Client::get_openssl_verify_result() const { + if (is_ssl_) { + return static_cast(*cli_).get_openssl_verify_result(); + } + return -1; // NOTE: -1 doesn't match any of X509_V_ERR_??? +} + +inline SSL_CTX *Client::ssl_context() const { + if (is_ssl_) { return static_cast(*cli_).ssl_context(); } + return nullptr; +} +#endif + +// ---------------------------------------------------------------------------- + +/* + * C++11/14/17 Streaming API + * + * This section provides iterator-style streaming functionality for C++11/14/17. + * For C++20 and later, a coroutine-based API with range-for syntax is + * available. + * + * Usage: + * httplib::Client cli("example.com"); + * auto result = httplib::stream::Get(cli, "/large-file"); + * if (result) { + * while (result.next()) { + * process(result.data(), result.size()); + * } + * } + */ + +namespace stream { + +class Result { +public: + Result() : chunk_size_(8192) {} + + explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192) + : handle_(std::move(handle)), chunk_size_(chunk_size) {} + + Result(Result &&other) noexcept + : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)), + current_size_(other.current_size_), chunk_size_(other.chunk_size_), + finished_(other.finished_) { + other.current_size_ = 0; + other.finished_ = true; + } + + Result &operator=(Result &&other) noexcept { + if (this != &other) { + handle_ = std::move(other.handle_); + buffer_ = std::move(other.buffer_); + current_size_ = other.current_size_; + chunk_size_ = other.chunk_size_; + finished_ = other.finished_; + other.current_size_ = 0; + other.finished_ = true; + } + return *this; + } + + Result(const Result &) = delete; + Result &operator=(const Result &) = delete; + + // Check if the result is valid (connection succeeded and response received) + bool is_valid() const { return handle_.is_valid(); } + explicit operator bool() const { return is_valid(); } + + // Response status code + int status() const { + return handle_.response ? handle_.response->status : -1; + } + + // Response headers + const Headers &headers() const { + static const Headers empty_headers; + return handle_.response ? handle_.response->headers : empty_headers; + } + + std::string get_header_value(const std::string &key, + const char *def = "") const { + return handle_.response ? handle_.response->get_header_value(key, def) + : def; + } + + bool has_header(const std::string &key) const { + return handle_.response ? handle_.response->has_header(key) : false; + } + + // Error information + Error error() const { return handle_.error; } + Error read_error() const { return handle_.get_read_error(); } + bool has_read_error() const { return handle_.has_read_error(); } + + // Streaming iteration API + // Call next() to read the next chunk, then access data via data()/size() + // Returns true if data was read, false when stream is exhausted + bool next() { + if (!handle_.is_valid() || finished_) { return false; } + + if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); } + + ssize_t n = handle_.read(&buffer_[0], chunk_size_); + if (n > 0) { + current_size_ = static_cast(n); + return true; + } + + current_size_ = 0; + finished_ = true; + return false; + } + + // Pointer to current chunk data (valid after next() returns true) + const char *data() const { return buffer_.data(); } + + // Size of current chunk (valid after next() returns true) + size_t size() const { return current_size_; } + + // Convenience method: read all remaining data into a string + std::string read_all() { + std::string result; + while (next()) { + result.append(data(), size()); + } + return result; + } + +private: + ClientImpl::StreamHandle handle_; + std::string buffer_; + size_t current_size_ = 0; + size_t chunk_size_; + bool finished_ = false; +}; + +// GET +template +inline Result Get(ClientType &cli, const std::string &path, + size_t chunk_size = 8192) { + return Result{cli.open_stream("GET", path), chunk_size}; +} + +template +inline Result Get(ClientType &cli, const std::string &path, + const Headers &headers, size_t chunk_size = 8192) { + return Result{cli.open_stream("GET", path, {}, headers), chunk_size}; +} + +template +inline Result Get(ClientType &cli, const std::string &path, + const Params ¶ms, size_t chunk_size = 8192) { + return Result{cli.open_stream("GET", path, params), chunk_size}; +} + +template +inline Result Get(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + size_t chunk_size = 8192) { + return Result{cli.open_stream("GET", path, params, headers), chunk_size}; +} + +// POST +template +inline Result Post(ClientType &cli, const std::string &path, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{cli.open_stream("POST", path, {}, {}, body, content_type), + chunk_size}; +} + +template +inline Result Post(ClientType &cli, const std::string &path, + const Headers &headers, const std::string &body, + const std::string &content_type, size_t chunk_size = 8192) { + return Result{cli.open_stream("POST", path, {}, headers, body, content_type), + chunk_size}; +} + +template +inline Result Post(ClientType &cli, const std::string &path, + const Params ¶ms, const std::string &body, + const std::string &content_type, size_t chunk_size = 8192) { + return Result{cli.open_stream("POST", path, params, {}, body, content_type), + chunk_size}; +} + +template +inline Result Post(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{ + cli.open_stream("POST", path, params, headers, body, content_type), + chunk_size}; +} + +// PUT +template +inline Result Put(ClientType &cli, const std::string &path, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{cli.open_stream("PUT", path, {}, {}, body, content_type), + chunk_size}; +} + +template +inline Result Put(ClientType &cli, const std::string &path, + const Headers &headers, const std::string &body, + const std::string &content_type, size_t chunk_size = 8192) { + return Result{cli.open_stream("PUT", path, {}, headers, body, content_type), + chunk_size}; +} + +template +inline Result Put(ClientType &cli, const std::string &path, + const Params ¶ms, const std::string &body, + const std::string &content_type, size_t chunk_size = 8192) { + return Result{cli.open_stream("PUT", path, params, {}, body, content_type), + chunk_size}; +} + +template +inline Result Put(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{ + cli.open_stream("PUT", path, params, headers, body, content_type), + chunk_size}; +} + +// PATCH +template +inline Result Patch(ClientType &cli, const std::string &path, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{cli.open_stream("PATCH", path, {}, {}, body, content_type), + chunk_size}; +} + +template +inline Result Patch(ClientType &cli, const std::string &path, + const Headers &headers, const std::string &body, + const std::string &content_type, size_t chunk_size = 8192) { + return Result{cli.open_stream("PATCH", path, {}, headers, body, content_type), + chunk_size}; +} + +template +inline Result Patch(ClientType &cli, const std::string &path, + const Params ¶ms, const std::string &body, + const std::string &content_type, size_t chunk_size = 8192) { + return Result{cli.open_stream("PATCH", path, params, {}, body, content_type), + chunk_size}; +} + +template +inline Result Patch(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{ + cli.open_stream("PATCH", path, params, headers, body, content_type), + chunk_size}; +} + +// DELETE +template +inline Result Delete(ClientType &cli, const std::string &path, + size_t chunk_size = 8192) { + return Result{cli.open_stream("DELETE", path), chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const Headers &headers, size_t chunk_size = 8192) { + return Result{cli.open_stream("DELETE", path, {}, headers), chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{cli.open_stream("DELETE", path, {}, {}, body, content_type), + chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const Headers &headers, const std::string &body, + const std::string &content_type, + size_t chunk_size = 8192) { + return Result{ + cli.open_stream("DELETE", path, {}, headers, body, content_type), + chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const Params ¶ms, size_t chunk_size = 8192) { + return Result{cli.open_stream("DELETE", path, params), chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + size_t chunk_size = 8192) { + return Result{cli.open_stream("DELETE", path, params, headers), chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const Params ¶ms, const std::string &body, + const std::string &content_type, + size_t chunk_size = 8192) { + return Result{cli.open_stream("DELETE", path, params, {}, body, content_type), + chunk_size}; +} + +template +inline Result Delete(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + const std::string &body, const std::string &content_type, + size_t chunk_size = 8192) { + return Result{ + cli.open_stream("DELETE", path, params, headers, body, content_type), + chunk_size}; +} + +// HEAD +template +inline Result Head(ClientType &cli, const std::string &path, + size_t chunk_size = 8192) { + return Result{cli.open_stream("HEAD", path), chunk_size}; +} + +template +inline Result Head(ClientType &cli, const std::string &path, + const Headers &headers, size_t chunk_size = 8192) { + return Result{cli.open_stream("HEAD", path, {}, headers), chunk_size}; +} + +template +inline Result Head(ClientType &cli, const std::string &path, + const Params ¶ms, size_t chunk_size = 8192) { + return Result{cli.open_stream("HEAD", path, params), chunk_size}; +} + +template +inline Result Head(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + size_t chunk_size = 8192) { + return Result{cli.open_stream("HEAD", path, params, headers), chunk_size}; +} + +// OPTIONS +template +inline Result Options(ClientType &cli, const std::string &path, + size_t chunk_size = 8192) { + return Result{cli.open_stream("OPTIONS", path), chunk_size}; +} + +template +inline Result Options(ClientType &cli, const std::string &path, + const Headers &headers, size_t chunk_size = 8192) { + return Result{cli.open_stream("OPTIONS", path, {}, headers), chunk_size}; +} + +template +inline Result Options(ClientType &cli, const std::string &path, + const Params ¶ms, size_t chunk_size = 8192) { + return Result{cli.open_stream("OPTIONS", path, params), chunk_size}; +} + +template +inline Result Options(ClientType &cli, const std::string &path, + const Params ¶ms, const Headers &headers, + size_t chunk_size = 8192) { + return Result{cli.open_stream("OPTIONS", path, params, headers), chunk_size}; +} + +} // namespace stream +} // namespace httplib + +#endif // CPPHTTPLIB_HTTPLIB_H From 6888fcb581bd7a595479210ca854416b4578e5d4 Mon Sep 17 00:00:00 2001 From: xxnuo <54252779+xxnuo@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:22:32 +0800 Subject: [PATCH 11/49] feat: server add default_gen_params to override default args (#1050) --- examples/common/common.hpp | 2 +- examples/server/README.md | 59 ++++++++++++++++++++++++++++++++++++++ examples/server/main.cpp | 57 +++++++++++++++++++++++++++--------- 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/examples/common/common.hpp b/examples/common/common.hpp index e508aba34..558817eea 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -1749,4 +1749,4 @@ uint8_t* load_image_from_memory(const char* image_bytes, int expected_height = 0, int expected_channel = 3) { return load_image_common(true, image_bytes, len, width, height, expected_width, expected_height, expected_channel); -} +} \ No newline at end of file diff --git a/examples/server/README.md b/examples/server/README.md index 533081af6..6393d841d 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -60,4 +60,63 @@ Context Options: --vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32) --vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1 (overrides --vae-tile-size) + +Default Generation Options: + -p, --prompt the prompt to render + -n, --negative-prompt the negative prompt (default: "") + -i, --init-img path to the init image + --end-img path to the end image, required by flf2v + --mask path to the mask image + --control-image path to control image, control net + --control-video path to control video frames, It must be a directory path. The video frames inside should be stored as images in + lexicographical (character) order. For example, if the control video path is + `frames`, the directory contain images such as 00.png, 01.png, ... etc. + --pm-id-images-dir path to PHOTOMAKER input id images dir + --pm-id-embed-path path to PHOTOMAKER v2 id embed + -H, --height image height, in pixel space (default: 512) + -W, --width image width, in pixel space (default: 512) + --steps number of sample steps (default: 20) + --high-noise-steps (high noise) number of sample steps (default: -1 = auto) + --clip-skip ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). <= 0 represents unspecified, + will be 1 for SD1.x, 2 for SD2.x + -b, --batch-count batch count + --video-frames video frames (default: 1) + --fps fps (default: 24) + --timestep-shift shift timestep for NitroFusion models (default: 0). recommended N for NitroSD-Realism around 250 and 500 for + NitroSD-Vibrant + --upscale-repeats Run the ESRGAN upscaler this many times (default: 1) + --upscale-tile-size tile size for ESRGAN upscaling (default: 128) + --cfg-scale unconditional guidance scale: (default: 7.0) + --img-cfg-scale image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale) + --guidance distilled guidance scale for models with guidance input (default: 3.5) + --slg-scale skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5 + medium + --skip-layer-start SLG enabling point (default: 0.01) + --skip-layer-end SLG disabling point (default: 0.2) + --eta eta in DDIM, only for DDIM and TCD (default: 0) + --high-noise-cfg-scale (high noise) unconditional guidance scale: (default: 7.0) + --high-noise-img-cfg-scale (high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale) + --high-noise-guidance (high noise) distilled guidance scale for models with guidance input (default: 3.5) + --high-noise-slg-scale (high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0) + --high-noise-skip-layer-start (high noise) SLG enabling point (default: 0.01) + --high-noise-skip-layer-end (high noise) SLG disabling point (default: 0.2) + --high-noise-eta (high noise) eta in DDIM, only for DDIM and TCD (default: 0) + --strength strength for noising/unnoising (default: 0.75) + --pm-style-strength + --control-strength strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image + --moe-boundary timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1 + --vace-strength wan vace strength + --increase-ref-index automatically increase the indices of references images based on the order they are listed (starting with 1). + --disable-auto-resize-ref-image disable auto resize of ref images + -s, --seed RNG seed (default: 42, use random seed for < 0) + --sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, + tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise) + --high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, + ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise + --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], + default: discrete + --skip-layers layers to skip for SLG steps (default: [7,8,9]) + --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) + -r, --ref-image reference image for Flux Kontext models (can be used multiple times) + --easycache enable EasyCache for DiT models with optional "threshold,start_percent,end_percent" (default: 0.2,0.15,0.95) ``` \ No newline at end of file diff --git a/examples/server/main.cpp b/examples/server/main.cpp index 3bfaa36d4..90cf484b9 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -179,17 +179,21 @@ void print_usage(int argc, const char* argv[], const std::vector& op options_list[0].print(); std::cout << "\nContext Options:\n"; options_list[1].print(); + std::cout << "\nDefault Generation Options:\n"; + options_list[2].print(); } -void parse_args(int argc, const char** argv, SDSvrParams& svr_params, SDContextParams& ctx_params) { - std::vector options_vec = {svr_params.get_options(), ctx_params.get_options()}; +void parse_args(int argc, const char** argv, SDSvrParams& svr_params, SDContextParams& ctx_params, SDGenerationParams& default_gen_params) { + std::vector options_vec = {svr_params.get_options(), ctx_params.get_options(), default_gen_params.get_options()}; if (!parse_options(argc, argv, options_vec)) { print_usage(argc, argv, options_vec); exit(svr_params.normal_exit ? 0 : 1); } - if (!svr_params.process_and_check() || !ctx_params.process_and_check(IMG_GEN)) { + if (!svr_params.process_and_check() || + !ctx_params.process_and_check(IMG_GEN) || + !default_gen_params.process_and_check(IMG_GEN, ctx_params.lora_model_dir)) { print_usage(argc, argv, options_vec); exit(1); } @@ -298,7 +302,8 @@ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { int main(int argc, const char** argv) { SDSvrParams svr_params; SDContextParams ctx_params; - parse_args(argc, argv, svr_params, ctx_params); + SDGenerationParams default_gen_params; + parse_args(argc, argv, svr_params, ctx_params, default_gen_params); sd_set_log_callback(sd_log_cb, (void*)&svr_params); @@ -306,6 +311,7 @@ int main(int argc, const char** argv) { printf("%s", sd_get_system_info()); printf("%s\n", svr_params.to_string().c_str()); printf("%s\n", ctx_params.to_string().c_str()); + printf("%s\n", default_gen_params.to_string().c_str()); } sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false, false, false); @@ -320,6 +326,23 @@ int main(int argc, const char** argv) { httplib::Server svr; + svr.set_pre_routing_handler([](const httplib::Request& req, httplib::Response& res) { + std::string origin = req.get_header_value("Origin"); + if (origin.empty()) { + origin = "*"; + } + res.set_header("Access-Control-Allow-Origin", origin); + res.set_header("Access-Control-Allow-Credentials", "true"); + res.set_header("Access-Control-Allow-Methods", "*"); + res.set_header("Access-Control-Allow-Headers", "*"); + + if (req.method == "OPTIONS") { + res.status = 204; + return httplib::Server::HandlerResponse::Handled; + } + return httplib::Server::HandlerResponse::Unhandled; + }); + // health svr.Get("/", [&](const httplib::Request&, httplib::Response& res) { res.set_content(R"({"ok":true,"service":"sd-cpp-http"})", "application/json"); @@ -390,11 +413,11 @@ int main(int argc, const char** argv) { out["data"] = json::array(); out["output_format"] = output_format; - SDGenerationParams gen_params; - gen_params.prompt = prompt; - gen_params.width = width; - gen_params.height = height; - gen_params.batch_count = n; + SDGenerationParams gen_params = default_gen_params; + gen_params.prompt = prompt; + gen_params.width = width; + gen_params.height = height; + gen_params.batch_count = n; if (!sd_cpp_extra_args_str.empty() && !gen_params.from_json_str(sd_cpp_extra_args_str)) { res.status = 400; @@ -570,11 +593,11 @@ int main(int argc, const char** argv) { output_compression = 0; } - SDGenerationParams gen_params; - gen_params.prompt = prompt; - gen_params.width = width; - gen_params.height = height; - gen_params.batch_count = n; + SDGenerationParams gen_params = default_gen_params; + gen_params.prompt = prompt; + gen_params.width = width; + gen_params.height = height; + gen_params.batch_count = n; if (!sd_cpp_extra_args_str.empty() && !gen_params.from_json_str(sd_cpp_extra_args_str)) { res.status = 400; @@ -582,6 +605,12 @@ int main(int argc, const char** argv) { return; } + if (!gen_params.process_and_check(IMG_GEN, ctx_params.lora_model_dir)) { + res.status = 400; + res.set_content(R"({"error":"invalid params"})", "application/json"); + return; + } + if (svr_params.verbose) { printf("%s\n", gen_params.to_string().c_str()); } From 15d0f82760e2d44d9bec904b277c4a7ad1f6b2ed Mon Sep 17 00:00:00 2001 From: leejet Date: Sat, 13 Dec 2025 14:27:47 +0800 Subject: [PATCH 12/49] feat(server): do not parse lora fromt client-side prompts (#1083) --- examples/common/common.hpp | 3 +++ examples/server/main.cpp | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 558817eea..0ab5c08a4 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -1349,6 +1349,9 @@ struct SDGenerationParams { } void extract_and_remove_lora(const std::string& lora_model_dir) { + if (lora_model_dir.empty()) { + return; + } static const std::regex re(R"(]+):([^>]+)>)"); static const std::vector valid_ext = {".pt", ".safetensors", ".gguf"}; std::smatch m; diff --git a/examples/server/main.cpp b/examples/server/main.cpp index 90cf484b9..f1ba0cd14 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -425,7 +425,7 @@ int main(int argc, const char** argv) { return; } - if (!gen_params.process_and_check(IMG_GEN, ctx_params.lora_model_dir)) { + if (!gen_params.process_and_check(IMG_GEN, "")) { res.status = 400; res.set_content(R"({"error":"invalid params"})", "application/json"); return; @@ -605,7 +605,7 @@ int main(int argc, const char** argv) { return; } - if (!gen_params.process_and_check(IMG_GEN, ctx_params.lora_model_dir)) { + if (!gen_params.process_and_check(IMG_GEN, "")) { res.status = 400; res.set_content(R"({"error":"invalid params"})", "application/json"); return; From 8f05f5bc6ee9d6aba9d1ff2be7739a5a3cf1586d Mon Sep 17 00:00:00 2001 From: rmatif Date: Sat, 13 Dec 2025 09:20:02 +0100 Subject: [PATCH 13/49] feat: add support for custom scheduler (#694) --------- Co-authored-by: leejet --- examples/cli/README.md | 1 + examples/cli/main.cpp | 12 ++++++++-- examples/common/common.hpp | 46 ++++++++++++++++++++++++++++++++++++ examples/server/README.md | 1 + stable-diffusion.cpp | 48 ++++++++++++++++++++++++++++++++------ stable-diffusion.h | 2 ++ 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/examples/cli/README.md b/examples/cli/README.md index f6a427851..02650f703 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -121,6 +121,7 @@ Generation Options: ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], default: discrete + --sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0"). --skip-layers layers to skip for SLG steps (default: [7,8,9]) --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) -r, --ref-image reference image for Flux Kontext models (can be used multiple times) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index eaa2591e6..417d211aa 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -258,7 +258,15 @@ std::string get_image_params(const SDCliParams& cli_params, const SDContextParam parameter_string += "Sampler RNG: " + std::string(sd_rng_type_name(ctx_params.sampler_rng_type)) + ", "; } parameter_string += "Sampler: " + std::string(sd_sample_method_name(gen_params.sample_params.sample_method)); - if (gen_params.sample_params.scheduler != SCHEDULER_COUNT) { + if (!gen_params.custom_sigmas.empty()) { + parameter_string += ", Custom Sigmas: ["; + for (size_t i = 0; i < gen_params.custom_sigmas.size(); ++i) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(4) << gen_params.custom_sigmas[i]; + parameter_string += oss.str() + (i == gen_params.custom_sigmas.size() - 1 ? "" : ", "); + } + parameter_string += "]"; + } else if (gen_params.sample_params.scheduler != SCHEDULER_COUNT) { // Only show schedule if not using custom sigmas parameter_string += " " + std::string(sd_scheduler_name(gen_params.sample_params.scheduler)); } parameter_string += ", "; @@ -806,4 +814,4 @@ int main(int argc, const char* argv[]) { release_all_resources(); return 0; -} +} \ No newline at end of file diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 0ab5c08a4..ccd01cec4 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -883,6 +883,8 @@ struct SDGenerationParams { std::vector high_noise_skip_layers = {7, 8, 9}; sd_sample_params_t high_noise_sample_params; + std::vector custom_sigmas; + std::string easycache_option; sd_easycache_params_t easycache_params; @@ -1201,6 +1203,43 @@ struct SDGenerationParams { return 1; }; + auto on_sigmas_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + std::string sigmas_str = argv[index]; + if (!sigmas_str.empty() && sigmas_str.front() == '[') { + sigmas_str.erase(0, 1); + } + if (!sigmas_str.empty() && sigmas_str.back() == ']') { + sigmas_str.pop_back(); + } + + std::stringstream ss(sigmas_str); + std::string item; + while (std::getline(ss, item, ',')) { + item.erase(0, item.find_first_not_of(" \t\n\r\f\v")); + item.erase(item.find_last_not_of(" \t\n\r\f\v") + 1); + if (!item.empty()) { + try { + custom_sigmas.push_back(std::stof(item)); + } catch (const std::invalid_argument& e) { + fprintf(stderr, "error: invalid float value '%s' in --sigmas\n", item.c_str()); + return -1; + } catch (const std::out_of_range& e) { + fprintf(stderr, "error: float value '%s' out of range in --sigmas\n", item.c_str()); + return -1; + } + } + } + + if (custom_sigmas.empty() && !sigmas_str.empty()) { + fprintf(stderr, "error: could not parse any sigma values from '%s'\n", argv[index]); + return -1; + } + return 1; + }; + auto on_ref_image_arg = [&](int argc, const char** argv, int index) { if (++index >= argc) { return -1; @@ -1260,6 +1299,10 @@ struct SDGenerationParams { "--scheduler", "denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], default: discrete", on_scheduler_arg}, + {"", + "--sigmas", + "custom sigma values for the sampler, comma-separated (e.g., \"14.61,7.8,3.5,0.0\").", + on_sigmas_arg}, {"", "--skip-layers", "layers to skip for SLG steps (default: [7,8,9])", @@ -1512,6 +1555,8 @@ struct SDGenerationParams { sample_params.guidance.slg.layers = skip_layers.data(); sample_params.guidance.slg.layer_count = skip_layers.size(); + sample_params.custom_sigmas = custom_sigmas.data(); + sample_params.custom_sigmas_count = static_cast(custom_sigmas.size()); high_noise_sample_params.guidance.slg.layers = high_noise_skip_layers.data(); high_noise_sample_params.guidance.slg.layer_count = high_noise_skip_layers.size(); @@ -1606,6 +1651,7 @@ struct SDGenerationParams { << " sample_params: " << sample_params_str << ",\n" << " high_noise_skip_layers: " << vec_to_string(high_noise_skip_layers) << ",\n" << " high_noise_sample_params: " << high_noise_sample_params_str << ",\n" + << " custom_sigmas: " << vec_to_string(custom_sigmas) << ",\n" << " easycache_option: \"" << easycache_option << "\",\n" << " easycache: " << (easycache_params.enabled ? "enabled" : "disabled") diff --git a/examples/server/README.md b/examples/server/README.md index 6393d841d..43c5d5f57 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -115,6 +115,7 @@ Default Generation Options: ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], default: discrete + --sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0"). --skip-layers layers to skip for SLG steps (default: [7,8,9]) --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) -r, --ref-image reference image for Flux Kontext models (can be used multiple times) diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 1ef851247..2cb588213 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -2600,6 +2600,8 @@ void sd_sample_params_init(sd_sample_params_t* sample_params) { sample_params->scheduler = SCHEDULER_COUNT; sample_params->sample_method = SAMPLE_METHOD_COUNT; sample_params->sample_steps = 20; + sample_params->custom_sigmas = nullptr; + sample_params->custom_sigmas_count = 0; } char* sd_sample_params_to_str(const sd_sample_params_t* sample_params) { @@ -3194,11 +3196,21 @@ sd_image_t* generate_image(sd_ctx_t* sd_ctx, const sd_img_gen_params_t* sd_img_g } LOG_INFO("sampling using %s method", sampling_methods_str[sample_method]); - int sample_steps = sd_img_gen_params->sample_params.sample_steps; - std::vector sigmas = sd_ctx->sd->denoiser->get_sigmas(sample_steps, - sd_ctx->sd->get_image_seq_len(height, width), - sd_img_gen_params->sample_params.scheduler, - sd_ctx->sd->version); + int sample_steps = sd_img_gen_params->sample_params.sample_steps; + std::vector sigmas; + if (sd_img_gen_params->sample_params.custom_sigmas_count > 0) { + sigmas = std::vector(sd_img_gen_params->sample_params.custom_sigmas, + sd_img_gen_params->sample_params.custom_sigmas + sd_img_gen_params->sample_params.custom_sigmas_count); + if (sample_steps != sigmas.size() - 1) { + sample_steps = static_cast(sigmas.size()) - 1; + LOG_WARN("sample_steps != custom_sigmas_count - 1, set sample_steps to %d", sample_steps); + } + } else { + sigmas = sd_ctx->sd->denoiser->get_sigmas(sample_steps, + sd_ctx->sd->get_image_seq_len(height, width), + sd_img_gen_params->sample_params.scheduler, + sd_ctx->sd->version); + } ggml_tensor* init_latent = nullptr; ggml_tensor* concat_latent = nullptr; @@ -3461,7 +3473,29 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s if (high_noise_sample_steps > 0) { total_steps += high_noise_sample_steps; } - std::vector sigmas = sd_ctx->sd->denoiser->get_sigmas(total_steps, 0, sd_vid_gen_params->sample_params.scheduler, sd_ctx->sd->version); + + std::vector sigmas; + if (sd_vid_gen_params->sample_params.custom_sigmas_count > 0) { + sigmas = std::vector(sd_vid_gen_params->sample_params.custom_sigmas, + sd_vid_gen_params->sample_params.custom_sigmas + sd_vid_gen_params->sample_params.custom_sigmas_count); + if (total_steps != sigmas.size() - 1) { + total_steps = static_cast(sigmas.size()) - 1; + LOG_WARN("total_steps != custom_sigmas_count - 1, set total_steps to %d", total_steps); + if (sample_steps >= total_steps) { + sample_steps = total_steps; + LOG_WARN("total_steps != custom_sigmas_count - 1, set sample_steps to %d", sample_steps); + } + if (high_noise_sample_steps > 0) { + high_noise_sample_steps = total_steps - sample_steps; + LOG_WARN("total_steps != custom_sigmas_count - 1, set high_noise_sample_steps to %d", high_noise_sample_steps); + } + } + } else { + sigmas = sd_ctx->sd->denoiser->get_sigmas(total_steps, + 0, + sd_vid_gen_params->sample_params.scheduler, + sd_ctx->sd->version); + } if (high_noise_sample_steps < 0) { // timesteps ∝ sigmas for Flow models (like wan2.2 a14b) @@ -3841,4 +3875,4 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s LOG_INFO("generate_video completed in %.2fs", (t5 - t0) * 1.0f / 1000); return result_images; -} +} \ No newline at end of file diff --git a/stable-diffusion.h b/stable-diffusion.h index 2da70bd77..e4abc8dcd 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -225,6 +225,8 @@ typedef struct { int sample_steps; float eta; int shifted_timestep; + float* custom_sigmas; + int custom_sigmas_count; } sd_sample_params_t; typedef struct { From d96b4152d692a2f28cfb1677e4939c1ca551a937 Mon Sep 17 00:00:00 2001 From: stduhpf Date: Sat, 13 Dec 2025 18:22:41 +0100 Subject: [PATCH 14/49] perf: optimize ggml_ext_chunk (#1084) --- common.hpp | 4 +++- ggml_extend.hpp | 34 +++++++++++----------------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/common.hpp b/common.hpp index 33d499fb1..74b218ab7 100644 --- a/common.hpp +++ b/common.hpp @@ -194,10 +194,12 @@ class GEGLU : public UnaryBlock { auto proj = std::dynamic_pointer_cast(blocks["proj"]); x = proj->forward(ctx, x); // [ne3, ne2, ne1, dim_out*2] - auto x_vec = ggml_ext_chunk(ctx->ggml_ctx, x, 2, 0); + auto x_vec = ggml_ext_chunk(ctx->ggml_ctx, x, 2, 0, false); x = x_vec[0]; // [ne3, ne2, ne1, dim_out] auto gate = x_vec[1]; // [ne3, ne2, ne1, dim_out] + gate = ggml_cont(ctx->ggml_ctx, gate); + gate = ggml_gelu_inplace(ctx->ggml_ctx, gate); x = ggml_mul(ctx->ggml_ctx, x, gate); // [ne3, ne2, ne1, dim_out] diff --git a/ggml_extend.hpp b/ggml_extend.hpp index 07b9bfbf0..26dff4993 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -732,34 +732,22 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_slice(struct ggml_context* ctx, __STATIC_INLINE__ std::vector ggml_ext_chunk(struct ggml_context* ctx, struct ggml_tensor* x, int num, - int64_t dim) { + int64_t dim, + bool cont = true) { GGML_ASSERT(dim >= 0 && dim < 4); GGML_ASSERT(x->ne[dim] % num == 0); - int perm[4] = {0, 1, 2, 3}; - for (int i = dim; i < 3; ++i) - perm[i] = perm[i + 1]; - perm[3] = dim; - - int inv_perm[4]; - for (int i = 0; i < 4; ++i) - inv_perm[perm[i]] = i; - - if (dim != 3) { - x = ggml_ext_torch_permute(ctx, x, perm[0], perm[1], perm[2], perm[3]); - x = ggml_cont(ctx, x); - } - std::vector chunks; - int64_t chunk_size = x->ne[3] / num; + int64_t chunk_size = x->ne[dim] / num; + int64_t stride = chunk_size * x->nb[dim]; + int64_t chunk_ne[4] = {x->ne[0], x->ne[1], x->ne[2], x->ne[3]}; + chunk_ne[dim] = chunk_size; for (int i = 0; i < num; i++) { auto chunk = ggml_view_4d( ctx, x, - x->ne[0], x->ne[1], x->ne[2], chunk_size, - x->nb[1], x->nb[2], x->nb[3], x->nb[3] * i * chunk_size); - - if (dim != 3) { - chunk = ggml_ext_torch_permute(ctx, chunk, inv_perm[0], inv_perm[1], inv_perm[2], inv_perm[3]); + chunk_ne[0], chunk_ne[1], chunk_ne[2], chunk_ne[3], + x->nb[1], x->nb[2], x->nb[3], stride * i); + if (cont) { chunk = ggml_cont(ctx, chunk); } chunks.push_back(chunk); @@ -772,7 +760,7 @@ __STATIC_INLINE__ ggml_tensor* ggml_ext_silu_act(ggml_context* ctx, ggml_tensor* // x: [ne3, ne2, ne1, ne0] // return: [ne3, ne2, ne1, ne0/2] - auto x_vec = ggml_ext_chunk(ctx, x, 2, 0); + auto x_vec = ggml_ext_chunk(ctx, x, 2, 0, false); ggml_tensor* gate; if (gate_first) { gate = x_vec[0]; @@ -781,7 +769,7 @@ __STATIC_INLINE__ ggml_tensor* ggml_ext_silu_act(ggml_context* ctx, ggml_tensor* x = x_vec[0]; gate = x_vec[1]; } - + gate = ggml_cont(ctx, gate); gate = ggml_silu_inplace(ctx, gate); x = ggml_mul(ctx, x, gate); // [ne3, ne2, ne1, ne0/2] From 614f8736df54bbf7a20ecb324a821d0e505c6503 Mon Sep 17 00:00:00 2001 From: "Kirill A. Korinsky" Date: Sat, 13 Dec 2025 18:23:34 +0100 Subject: [PATCH 15/49] sync: update ggml (#1082) --- ggml | 2 +- ggml_extend.hpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ggml b/ggml index 2d3876d55..f5425c0ee 160000 --- a/ggml +++ b/ggml @@ -1 +1 @@ -Subproject commit 2d3876d554551d35c06dccc5852be50d5fd2a275 +Subproject commit f5425c0ee5e582a7d64411f06139870bff3e52e0 diff --git a/ggml_extend.hpp b/ggml_extend.hpp index 26dff4993..28fd0184f 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -1270,6 +1270,9 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_attention_ext(struct ggml_context } if (mask_in != nullptr) { + // the need for padding got removed in ggml 4767bda + // ensure we can still use the old version for now +#ifdef GGML_KQ_MASK_PAD int mask_pad = 0; if (mask_in->ne[1] % GGML_KQ_MASK_PAD != 0) { mask_pad = GGML_PAD(L_q, GGML_KQ_MASK_PAD) - mask_in->ne[1]; @@ -1277,6 +1280,7 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_attention_ext(struct ggml_context if (mask_pad > 0) { mask_in = ggml_pad(ctx, mask_in, 0, mask_pad, 0, 0); } +#endif mask_in = ggml_cast(ctx, mask_in, GGML_TYPE_F16); } From 43a70e819b9254dee0d017305d6992f6bb27f850 Mon Sep 17 00:00:00 2001 From: leejet Date: Sun, 14 Dec 2025 01:24:15 +0800 Subject: [PATCH 16/49] fix: add lora info to image metadata (#1086) --- examples/cli/main.cpp | 2 +- examples/common/common.hpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 417d211aa..22480d7eb 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -232,7 +232,7 @@ static std::string sd_basename(const std::string& path) { } std::string get_image_params(const SDCliParams& cli_params, const SDContextParams& ctx_params, const SDGenerationParams& gen_params, int64_t seed) { - std::string parameter_string = gen_params.prompt + "\n"; + std::string parameter_string = gen_params.prompt_with_lora + "\n"; if (gen_params.negative_prompt.size() != 0) { parameter_string += "Negative prompt: " + gen_params.negative_prompt + "\n"; } diff --git a/examples/common/common.hpp b/examples/common/common.hpp index ccd01cec4..bf38379d2 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -863,6 +863,7 @@ static bool is_absolute_path(const std::string& p) { struct SDGenerationParams { std::string prompt; + std::string prompt_with_lora; // for metadata record only std::string negative_prompt; int clip_skip = -1; // <= 0 represents unspecified int width = 512; @@ -1476,6 +1477,7 @@ struct SDGenerationParams { } bool process_and_check(SDMode mode, const std::string& lora_model_dir) { + prompt_with_lora = prompt; if (width <= 0) { fprintf(stderr, "error: the width must be greater than 0\n"); return false; From 200cb6f2ca07e40fa83b610a4e595f4da06ec709 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Mon, 15 Dec 2025 12:51:40 -0300 Subject: [PATCH 17/49] fix: avoid crash with VAE tiling and certain image sizes (#1090) --- ggml_extend.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/ggml_extend.hpp b/ggml_extend.hpp index 28fd0184f..fcaa92c9e 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -848,8 +848,6 @@ __STATIC_INLINE__ void sd_tiling_non_square(ggml_tensor* input, LOG_DEBUG("num tiles : %d, %d ", num_tiles_x, num_tiles_y); LOG_DEBUG("optimal overlap : %f, %f (targeting %f)", tile_overlap_factor_x, tile_overlap_factor_y, tile_overlap_factor); - GGML_ASSERT(input_width % 2 == 0 && input_height % 2 == 0 && output_width % 2 == 0 && output_height % 2 == 0); // should be multiple of 2 - int tile_overlap_x = (int32_t)(p_tile_size_x * tile_overlap_factor_x); int non_tile_overlap_x = p_tile_size_x - tile_overlap_x; From e687913bf11520a1ecfeff92eabc7e8079ecded4 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Tue, 16 Dec 2025 11:37:45 -0300 Subject: [PATCH 18/49] chore: remove lora_model_dir parameter (#1100) --- examples/common/common.hpp | 1 - stable-diffusion.cpp | 4 ---- stable-diffusion.h | 1 - 3 files changed, 6 deletions(-) diff --git a/examples/common/common.hpp b/examples/common/common.hpp index bf38379d2..448917140 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -796,7 +796,6 @@ struct SDContextParams { vae_path.c_str(), taesd_path.c_str(), control_net_path.c_str(), - lora_model_dir.c_str(), embedding_vec.data(), static_cast(embedding_vec.size()), photo_maker_path.c_str(), diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 2cb588213..74bcc4c85 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -134,7 +134,6 @@ class StableDiffusionGGML { std::map tensors; - std::string lora_model_dir; // lora_name => multiplier std::unordered_map curr_lora_state; @@ -206,7 +205,6 @@ class StableDiffusionGGML { n_threads = sd_ctx_params->n_threads; vae_decode_only = sd_ctx_params->vae_decode_only; free_params_immediately = sd_ctx_params->free_params_immediately; - lora_model_dir = SAFE_STR(sd_ctx_params->lora_model_dir); taesd_path = SAFE_STR(sd_ctx_params->taesd_path); use_tiny_autoencoder = taesd_path.size() > 0; offload_params_to_cpu = sd_ctx_params->offload_params_to_cpu; @@ -2536,7 +2534,6 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) { "vae_path: %s\n" "taesd_path: %s\n" "control_net_path: %s\n" - "lora_model_dir: %s\n" "photo_maker_path: %s\n" "tensor_type_rules: %s\n" "vae_decode_only: %s\n" @@ -2566,7 +2563,6 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) { SAFE_STR(sd_ctx_params->vae_path), SAFE_STR(sd_ctx_params->taesd_path), SAFE_STR(sd_ctx_params->control_net_path), - SAFE_STR(sd_ctx_params->lora_model_dir), SAFE_STR(sd_ctx_params->photo_maker_path), SAFE_STR(sd_ctx_params->tensor_type_rules), BOOL_STR(sd_ctx_params->vae_decode_only), diff --git a/stable-diffusion.h b/stable-diffusion.h index e4abc8dcd..9266ba437 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -168,7 +168,6 @@ typedef struct { const char* vae_path; const char* taesd_path; const char* control_net_path; - const char* lora_model_dir; const sd_embedding_t* embeddings; uint32_t embedding_count; const char* photo_maker_path; From a23262dfdefeb3f441de7a580bb620b737c941b6 Mon Sep 17 00:00:00 2001 From: akleine Date: Tue, 16 Dec 2025 15:45:10 +0100 Subject: [PATCH 19/49] fix: added a clean exit in ModelLoader::load_tensors if OOM (#1097) --- examples/common/common.hpp | 2 +- model.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 448917140..7ea070c5c 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -862,7 +862,7 @@ static bool is_absolute_path(const std::string& p) { struct SDGenerationParams { std::string prompt; - std::string prompt_with_lora; // for metadata record only + std::string prompt_with_lora; // for metadata record only std::string negative_prompt; int clip_skip = -1; // <= 0 represents unspecified int width = 512; diff --git a/model.cpp b/model.cpp index 0480efefb..01a8c45de 100644 --- a/model.cpp +++ b/model.cpp @@ -1520,6 +1520,11 @@ bool ModelLoader::load_tensors(on_new_tensor_cb_t on_new_tensor_cb, int n_thread i64_to_i32_vec((int64_t*)read_buf, (int32_t*)target_buf, tensor_storage.nelements()); } if (tensor_storage.type != dst_tensor->type) { + if (convert_buf == nullptr) { + LOG_ERROR("read tensor data failed: too less memory for conversion"); + failed = true; + return; + } convert_tensor((void*)target_buf, tensor_storage.type, convert_buf, From 9fa7f415df8ad2b2eb19650f515628e0e86a1f12 Mon Sep 17 00:00:00 2001 From: stduhpf Date: Tue, 16 Dec 2025 15:57:34 +0100 Subject: [PATCH 20/49] feat: add taehv support for Wan/Qwen (#937) --- examples/cli/README.md | 1 + examples/common/common.hpp | 4 + examples/server/README.md | 1 + stable-diffusion.cpp | 52 +++-- tae.hpp | 400 ++++++++++++++++++++++++++++++++++++- 5 files changed, 432 insertions(+), 26 deletions(-) diff --git a/examples/cli/README.md b/examples/cli/README.md index 02650f703..8531b2aed 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -31,6 +31,7 @@ Context Options: --high-noise-diffusion-model path to the standalone high noise diffusion model --vae path to standalone vae model --taesd path to taesd. Using Tiny AutoEncoder for fast decoding (low quality) + --tae alias of --taesd --control-net path to control net model --embd-dir embeddings directory --lora-model-dir lora model directory diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 7ea070c5c..2d890af8a 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -406,6 +406,10 @@ struct SDContextParams { "--taesd", "path to taesd. Using Tiny AutoEncoder for fast decoding (low quality)", &taesd_path}, + {"", + "--tae", + "alias of --taesd", + &taesd_path}, {"", "--control-net", "path to control net model", diff --git a/examples/server/README.md b/examples/server/README.md index 43c5d5f57..a475856fa 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -24,6 +24,7 @@ Context Options: --high-noise-diffusion-model path to the standalone high noise diffusion model --vae path to standalone vae model --taesd path to taesd. Using Tiny AutoEncoder for fast decoding (low quality) + --tae alias of --taesd --control-net path to control net model --embd-dir embeddings directory --lora-model-dir lora model directory diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 74bcc4c85..44bd3ccac 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -562,14 +562,27 @@ class StableDiffusionGGML { } if (sd_version_is_wan(version) || sd_version_is_qwen_image(version)) { - first_stage_model = std::make_shared(vae_backend, - offload_params_to_cpu, - tensor_storage_map, - "first_stage_model", - vae_decode_only, - version); - first_stage_model->alloc_params_buffer(); - first_stage_model->get_param_tensors(tensors, "first_stage_model"); + if (!use_tiny_autoencoder) { + first_stage_model = std::make_shared(vae_backend, + offload_params_to_cpu, + tensor_storage_map, + "first_stage_model", + vae_decode_only, + version); + first_stage_model->alloc_params_buffer(); + first_stage_model->get_param_tensors(tensors, "first_stage_model"); + } else { + tae_first_stage = std::make_shared(vae_backend, + offload_params_to_cpu, + tensor_storage_map, + "decoder", + vae_decode_only, + version); + if (sd_ctx_params->vae_conv_direct) { + LOG_INFO("Using Conv2d direct in the tae model"); + tae_first_stage->set_conv2d_direct_enabled(true); + } + } } else if (version == VERSION_CHROMA_RADIANCE) { first_stage_model = std::make_shared(vae_backend, offload_params_to_cpu); @@ -596,14 +609,13 @@ class StableDiffusionGGML { } first_stage_model->alloc_params_buffer(); first_stage_model->get_param_tensors(tensors, "first_stage_model"); - } - if (use_tiny_autoencoder) { - tae_first_stage = std::make_shared(vae_backend, - offload_params_to_cpu, - tensor_storage_map, - "decoder.layers", - vae_decode_only, - version); + } else if (use_tiny_autoencoder) { + tae_first_stage = std::make_shared(vae_backend, + offload_params_to_cpu, + tensor_storage_map, + "decoder.layers", + vae_decode_only, + version); if (sd_ctx_params->vae_conv_direct) { LOG_INFO("Using Conv2d direct in the tae model"); tae_first_stage->set_conv2d_direct_enabled(true); @@ -3614,7 +3626,8 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s denoise_mask = ggml_new_tensor_4d(work_ctx, GGML_TYPE_F32, init_latent->ne[0], init_latent->ne[1], init_latent->ne[2], 1); ggml_set_f32(denoise_mask, 1.f); - sd_ctx->sd->process_latent_out(init_latent); + if (!sd_ctx->sd->use_tiny_autoencoder) + sd_ctx->sd->process_latent_out(init_latent); ggml_ext_tensor_iter(init_image_latent, [&](ggml_tensor* t, int64_t i0, int64_t i1, int64_t i2, int64_t i3) { float value = ggml_ext_tensor_get_f32(t, i0, i1, i2, i3); @@ -3624,7 +3637,8 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s } }); - sd_ctx->sd->process_latent_in(init_latent); + if (!sd_ctx->sd->use_tiny_autoencoder) + sd_ctx->sd->process_latent_in(init_latent); int64_t t2 = ggml_time_ms(); LOG_INFO("encode_first_stage completed, taking %" PRId64 " ms", t2 - t1); @@ -3847,7 +3861,7 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s struct ggml_tensor* vid = sd_ctx->sd->decode_first_stage(work_ctx, final_latent, true); int64_t t5 = ggml_time_ms(); LOG_INFO("decode_first_stage completed, taking %.2fs", (t5 - t4) * 1.0f / 1000); - if (sd_ctx->sd->free_params_immediately) { + if (sd_ctx->sd->free_params_immediately && !sd_ctx->sd->use_tiny_autoencoder) { sd_ctx->sd->first_stage_model->free_params_buffer(); } diff --git a/tae.hpp b/tae.hpp index 7f3ca449a..5da76e692 100644 --- a/tae.hpp +++ b/tae.hpp @@ -162,6 +162,311 @@ class TinyDecoder : public UnaryBlock { } }; +class TPool : public UnaryBlock { + int stride; + +public: + TPool(int channels, int stride) + : stride(stride) { + blocks["conv"] = std::shared_ptr(new Conv2d(channels * stride, channels, {1, 1}, {1, 1}, {0, 0}, {1, 1}, false)); + } + + struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* x) override { + auto conv = std::dynamic_pointer_cast(blocks["conv"]); + auto h = x; + if (stride != 1) { + h = ggml_reshape_4d(ctx->ggml_ctx, h, h->ne[0], h->ne[1], h->ne[2] * stride, h->ne[3] / stride); + } + h = conv->forward(ctx, h); + return h; + } +}; + +class TGrow : public UnaryBlock { + int stride; + +public: + TGrow(int channels, int stride) + : stride(stride) { + blocks["conv"] = std::shared_ptr(new Conv2d(channels, channels * stride, {1, 1}, {1, 1}, {0, 0}, {1, 1}, false)); + } + + struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* x) override { + auto conv = std::dynamic_pointer_cast(blocks["conv"]); + auto h = conv->forward(ctx, x); + if (stride != 1) { + h = ggml_reshape_4d(ctx->ggml_ctx, h, h->ne[0], h->ne[1], h->ne[2] / stride, h->ne[3] * stride); + } + return h; + } +}; + +class MemBlock : public GGMLBlock { + bool has_skip_conv = false; + +public: + MemBlock(int channels, int out_channels) + : has_skip_conv(channels != out_channels) { + blocks["conv.0"] = std::shared_ptr(new Conv2d(channels * 2, out_channels, {3, 3}, {1, 1}, {1, 1})); + blocks["conv.2"] = std::shared_ptr(new Conv2d(out_channels, out_channels, {3, 3}, {1, 1}, {1, 1})); + blocks["conv.4"] = std::shared_ptr(new Conv2d(out_channels, out_channels, {3, 3}, {1, 1}, {1, 1})); + if (has_skip_conv) { + blocks["skip"] = std::shared_ptr(new Conv2d(channels, out_channels, {1, 1}, {1, 1}, {0, 0}, {1, 1}, false)); + } + } + + struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* x, struct ggml_tensor* past) { + // x: [n, channels, h, w] + auto conv0 = std::dynamic_pointer_cast(blocks["conv.0"]); + auto conv1 = std::dynamic_pointer_cast(blocks["conv.2"]); + auto conv2 = std::dynamic_pointer_cast(blocks["conv.4"]); + + auto h = ggml_concat(ctx->ggml_ctx, x, past, 2); + h = conv0->forward(ctx, h); + h = ggml_relu_inplace(ctx->ggml_ctx, h); + h = conv1->forward(ctx, h); + h = ggml_relu_inplace(ctx->ggml_ctx, h); + h = conv2->forward(ctx, h); + + auto skip = x; + if (has_skip_conv) { + auto skip_conv = std::dynamic_pointer_cast(blocks["skip"]); + skip = skip_conv->forward(ctx, x); + } + h = ggml_add_inplace(ctx->ggml_ctx, h, skip); + h = ggml_relu_inplace(ctx->ggml_ctx, h); + return h; + } +}; + +struct ggml_tensor* patchify(struct ggml_context* ctx, + struct ggml_tensor* x, + int64_t patch_size, + int64_t b = 1) { + // x: [f, b*c, h*q, w*r] + // return: [f, b*c*r*q, h, w] + if (patch_size == 1) { + return x; + } + int64_t r = patch_size; + int64_t q = patch_size; + + int64_t W = x->ne[0]; + int64_t H = x->ne[1]; + int64_t C = x->ne[2]; + int64_t f = x->ne[3]; + + int64_t w = W / r; + int64_t h = H / q; + + x = ggml_reshape_4d(ctx, x, W, q, h, C * f); // [W, q, h, C*f] + x = ggml_ext_cont(ctx, ggml_ext_torch_permute(ctx, x, 0, 2, 1, 3)); // [W, h, q, C*f] + x = ggml_reshape_4d(ctx, x, r, w, h, q * C * f); // [r, w, h, q*C*f] + x = ggml_ext_cont(ctx, ggml_ext_torch_permute(ctx, x, 1, 2, 0, 3)); // [w, h, r, q*C*f] + x = ggml_reshape_4d(ctx, x, w, h, r * q * C, f); // [f, b*c*r*q, h, w] + + return x; +} + +struct ggml_tensor* unpatchify(struct ggml_context* ctx, + struct ggml_tensor* x, + int64_t patch_size, + int64_t b = 1) { + // x: [f, b*c*r*q, h, w] + // return: [f, b*c, h*q, w*r] + if (patch_size == 1) { + return x; + } + int64_t r = patch_size; + int64_t q = patch_size; + int64_t c = x->ne[2] / b / q / r; + int64_t f = x->ne[3]; + int64_t h = x->ne[1]; + int64_t w = x->ne[0]; + + x = ggml_reshape_4d(ctx, x, w, h, r, q * c * b * f); // [q*c*b*f, r, h, w] + x = ggml_ext_cont(ctx, ggml_ext_torch_permute(ctx, x, 2, 0, 1, 3)); // [r, w, h, q*c*b*f] + x = ggml_reshape_4d(ctx, x, r * w, h, q, c * b * f); // [c*b*f, q, h, r*w] + x = ggml_ext_cont(ctx, ggml_ext_torch_permute(ctx, x, 0, 2, 1, 3)); // [r*w, q, h, c*b*f] + x = ggml_reshape_4d(ctx, x, r * w, q * h, c * b, f); + + return x; +} + +class TinyVideoEncoder : public UnaryBlock { + int in_channels = 3; + int hidden = 64; + int z_channels = 4; + int num_blocks = 3; + int num_layers = 3; + int patch_size = 1; + +public: + TinyVideoEncoder(int z_channels = 4, int patch_size = 1) + : z_channels(z_channels), patch_size(patch_size) { + int index = 0; + blocks[std::to_string(index++)] = std::shared_ptr(new Conv2d(in_channels * patch_size * patch_size, hidden, {3, 3}, {1, 1}, {1, 1})); + index++; // nn.ReLU() + for (int i = 0; i < num_layers; i++) { + int stride = i == num_layers - 1 ? 1 : 2; + blocks[std::to_string(index++)] = std::shared_ptr(new TPool(hidden, stride)); + blocks[std::to_string(index++)] = std::shared_ptr(new Conv2d(hidden, hidden, {3, 3}, {2, 2}, {1, 1}, {1, 1}, false)); + for (int j = 0; j < num_blocks; j++) { + blocks[std::to_string(index++)] = std::shared_ptr(new MemBlock(hidden, hidden)); + } + } + blocks[std::to_string(index)] = std::shared_ptr(new Conv2d(hidden, z_channels, {3, 3}, {1, 1}, {1, 1})); + } + + struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* z) override { + auto first_conv = std::dynamic_pointer_cast(blocks["0"]); + + if (patch_size > 1) { + z = patchify(ctx->ggml_ctx, z, patch_size, 1); + } + + auto h = first_conv->forward(ctx, z); + h = ggml_relu_inplace(ctx->ggml_ctx, h); + + int index = 2; + for (int i = 0; i < num_layers; i++) { + auto pool = std::dynamic_pointer_cast(blocks[std::to_string(index++)]); + auto conv = std::dynamic_pointer_cast(blocks[std::to_string(index++)]); + + h = pool->forward(ctx, h); + h = conv->forward(ctx, h); + for (int j = 0; j < num_blocks; j++) { + auto block = std::dynamic_pointer_cast(blocks[std::to_string(index++)]); + auto mem = ggml_pad_ext(ctx->ggml_ctx, h, 0, 0, 0, 0, 0, 0, 1, 0); + mem = ggml_view_4d(ctx->ggml_ctx, mem, h->ne[0], h->ne[1], h->ne[2], h->ne[3], h->nb[1], h->nb[2], h->nb[3], 0); + h = block->forward(ctx, h, mem); + } + } + auto last_conv = std::dynamic_pointer_cast(blocks[std::to_string(index)]); + h = last_conv->forward(ctx, h); + return h; + } +}; + +class TinyVideoDecoder : public UnaryBlock { + int z_channels = 4; + int out_channels = 3; + int num_blocks = 3; + static const int num_layers = 3; + int channels[num_layers + 1] = {256, 128, 64, 64}; + int patch_size = 1; + +public: + TinyVideoDecoder(int z_channels = 4, int patch_size = 1) + : z_channels(z_channels), patch_size(patch_size) { + int index = 1; // Clamp() + blocks[std::to_string(index++)] = std::shared_ptr(new Conv2d(z_channels, channels[0], {3, 3}, {1, 1}, {1, 1})); + index++; // nn.ReLU() + for (int i = 0; i < num_layers; i++) { + int stride = i == 0 ? 1 : 2; + for (int j = 0; j < num_blocks; j++) { + blocks[std::to_string(index++)] = std::shared_ptr(new MemBlock(channels[i], channels[i])); + } + index++; // nn.Upsample() + blocks[std::to_string(index++)] = std::shared_ptr(new TGrow(channels[i], stride)); + blocks[std::to_string(index++)] = std::shared_ptr(new Conv2d(channels[i], channels[i + 1], {3, 3}, {1, 1}, {1, 1}, {1, 1}, false)); + } + index++; // nn.ReLU() + blocks[std::to_string(index++)] = std::shared_ptr(new Conv2d(channels[num_layers], out_channels * patch_size * patch_size, {3, 3}, {1, 1}, {1, 1})); + } + + struct ggml_tensor* forward(GGMLRunnerContext* ctx, struct ggml_tensor* z) override { + auto first_conv = std::dynamic_pointer_cast(blocks["1"]); + + // Clamp() + auto h = ggml_scale_inplace(ctx->ggml_ctx, + ggml_tanh_inplace(ctx->ggml_ctx, + ggml_scale(ctx->ggml_ctx, z, 1.0f / 3.0f)), + 3.0f); + + h = first_conv->forward(ctx, h); + h = ggml_relu_inplace(ctx->ggml_ctx, h); + int index = 3; + for (int i = 0; i < num_layers; i++) { + for (int j = 0; j < num_blocks; j++) { + auto block = std::dynamic_pointer_cast(blocks[std::to_string(index++)]); + auto mem = ggml_pad_ext(ctx->ggml_ctx, h, 0, 0, 0, 0, 0, 0, 1, 0); + mem = ggml_view_4d(ctx->ggml_ctx, mem, h->ne[0], h->ne[1], h->ne[2], h->ne[3], h->nb[1], h->nb[2], h->nb[3], 0); + h = block->forward(ctx, h, mem); + } + // upsample + index++; + h = ggml_upscale(ctx->ggml_ctx, h, 2, GGML_SCALE_MODE_NEAREST); + auto block = std::dynamic_pointer_cast(blocks[std::to_string(index++)]); + h = block->forward(ctx, h); + block = std::dynamic_pointer_cast(blocks[std::to_string(index++)]); + h = block->forward(ctx, h); + } + h = ggml_relu_inplace(ctx->ggml_ctx, h); + + auto last_conv = std::dynamic_pointer_cast(blocks[std::to_string(++index)]); + h = last_conv->forward(ctx, h); + if (patch_size > 1) { + h = unpatchify(ctx->ggml_ctx, h, patch_size, 1); + } + // shape(W, H, 3, 3 + T) => shape(W, H, 3, T) + h = ggml_view_4d(ctx->ggml_ctx, h, h->ne[0], h->ne[1], h->ne[2], h->ne[3] - 3, h->nb[1], h->nb[2], h->nb[3], 3 * h->nb[3]); + return h; + } +}; + +class TAEHV : public GGMLBlock { +protected: + bool decode_only; + SDVersion version; + +public: + TAEHV(bool decode_only = true, SDVersion version = VERSION_WAN2) + : decode_only(decode_only), version(version) { + int z_channels = 16; + int patch = 1; + if (version == VERSION_WAN2_2_TI2V) { + z_channels = 48; + patch = 2; + } + blocks["decoder"] = std::shared_ptr(new TinyVideoDecoder(z_channels, patch)); + if (!decode_only) { + blocks["encoder"] = std::shared_ptr(new TinyVideoEncoder(z_channels, patch)); + } + } + + struct ggml_tensor* decode(GGMLRunnerContext* ctx, struct ggml_tensor* z) { + auto decoder = std::dynamic_pointer_cast(blocks["decoder"]); + if (sd_version_is_wan(version)) { + // (W, H, C, T) -> (W, H, T, C) + z = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, z, 0, 1, 3, 2)); + } + auto result = decoder->forward(ctx, z); + if (sd_version_is_wan(version)) { + // (W, H, C, T) -> (W, H, T, C) + result = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, result, 0, 1, 3, 2)); + } + return result; + } + + struct ggml_tensor* encode(GGMLRunnerContext* ctx, struct ggml_tensor* x) { + auto encoder = std::dynamic_pointer_cast(blocks["encoder"]); + // (W, H, T, C) -> (W, H, C, T) + x = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, x, 0, 1, 3, 2)); + int64_t num_frames = x->ne[3]; + if (num_frames % 4) { + // pad to multiple of 4 at the end + auto last_frame = ggml_view_4d(ctx->ggml_ctx, x, x->ne[0], x->ne[1], x->ne[2], 1, x->nb[1], x->nb[2], x->nb[3], (num_frames - 1) * x->nb[3]); + for (int i = 0; i < 4 - num_frames % 4; i++) { + x = ggml_concat(ctx->ggml_ctx, x, last_frame, 3); + } + } + x = encoder->forward(ctx, x); + x = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, x, 0, 1, 3, 2)); + return x; + } +}; + class TAESD : public GGMLBlock { protected: bool decode_only; @@ -192,18 +497,30 @@ class TAESD : public GGMLBlock { }; struct TinyAutoEncoder : public GGMLRunner { + TinyAutoEncoder(ggml_backend_t backend, bool offload_params_to_cpu) + : GGMLRunner(backend, offload_params_to_cpu) {} + virtual bool compute(const int n_threads, + struct ggml_tensor* z, + bool decode_graph, + struct ggml_tensor** output, + struct ggml_context* output_ctx = nullptr) = 0; + + virtual bool load_from_file(const std::string& file_path, int n_threads) = 0; +}; + +struct TinyImageAutoEncoder : public TinyAutoEncoder { TAESD taesd; bool decode_only = false; - TinyAutoEncoder(ggml_backend_t backend, - bool offload_params_to_cpu, - const String2TensorStorage& tensor_storage_map, - const std::string prefix, - bool decoder_only = true, - SDVersion version = VERSION_SD1) + TinyImageAutoEncoder(ggml_backend_t backend, + bool offload_params_to_cpu, + const String2TensorStorage& tensor_storage_map, + const std::string prefix, + bool decoder_only = true, + SDVersion version = VERSION_SD1) : decode_only(decoder_only), taesd(decoder_only, version), - GGMLRunner(backend, offload_params_to_cpu) { + TinyAutoEncoder(backend, offload_params_to_cpu) { taesd.init(params_ctx, tensor_storage_map, prefix); } @@ -260,4 +577,73 @@ struct TinyAutoEncoder : public GGMLRunner { } }; +struct TinyVideoAutoEncoder : public TinyAutoEncoder { + TAEHV taehv; + bool decode_only = false; + + TinyVideoAutoEncoder(ggml_backend_t backend, + bool offload_params_to_cpu, + const String2TensorStorage& tensor_storage_map, + const std::string prefix, + bool decoder_only = true, + SDVersion version = VERSION_WAN2) + : decode_only(decoder_only), + taehv(decoder_only, version), + TinyAutoEncoder(backend, offload_params_to_cpu) { + taehv.init(params_ctx, tensor_storage_map, prefix); + } + + std::string get_desc() override { + return "taehv"; + } + + bool load_from_file(const std::string& file_path, int n_threads) { + LOG_INFO("loading taehv from '%s', decode_only = %s", file_path.c_str(), decode_only ? "true" : "false"); + alloc_params_buffer(); + std::map taehv_tensors; + taehv.get_param_tensors(taehv_tensors); + std::set ignore_tensors; + if (decode_only) { + ignore_tensors.insert("encoder."); + } + + ModelLoader model_loader; + if (!model_loader.init_from_file(file_path)) { + LOG_ERROR("init taehv model loader from file failed: '%s'", file_path.c_str()); + return false; + } + + bool success = model_loader.load_tensors(taehv_tensors, ignore_tensors, n_threads); + + if (!success) { + LOG_ERROR("load tae tensors from model loader failed"); + return false; + } + + LOG_INFO("taehv model loaded"); + return success; + } + + struct ggml_cgraph* build_graph(struct ggml_tensor* z, bool decode_graph) { + struct ggml_cgraph* gf = ggml_new_graph(compute_ctx); + z = to_backend(z); + auto runner_ctx = get_context(); + struct ggml_tensor* out = decode_graph ? taehv.decode(&runner_ctx, z) : taehv.encode(&runner_ctx, z); + ggml_build_forward_expand(gf, out); + return gf; + } + + bool compute(const int n_threads, + struct ggml_tensor* z, + bool decode_graph, + struct ggml_tensor** output, + struct ggml_context* output_ctx = nullptr) { + auto get_graph = [&]() -> struct ggml_cgraph* { + return build_graph(z, decode_graph); + }; + + return GGMLRunner::compute(get_graph, n_threads, false, output, output_ctx); + } +}; + #endif // __TAE_HPP__ \ No newline at end of file From ebe9d26a722610364676489ab17547cba4ceb1f5 Mon Sep 17 00:00:00 2001 From: leejet Date: Tue, 16 Dec 2025 23:00:41 +0800 Subject: [PATCH 21/49] feat: supports correct UTF-8 printing on windows (#1101) --- examples/cli/main.cpp | 129 +++++++----------------- examples/common/common.hpp | 196 ++++++++++++++++++++++++++++--------- examples/server/main.cpp | 68 +++---------- 3 files changed, 204 insertions(+), 189 deletions(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 22480d7eb..46260b173 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -106,9 +106,8 @@ struct SDCliParams { } } if (mode_found == -1) { - fprintf(stderr, - "error: invalid mode %s, must be one of [%s]\n", - mode_c_str, SD_ALL_MODES_STR); + LOG_ERROR("error: invalid mode %s, must be one of [%s]\n", + mode_c_str, SD_ALL_MODES_STR); exit(1); } mode = (SDMode)mode_found; @@ -128,8 +127,7 @@ struct SDCliParams { } } if (preview_found == -1) { - fprintf(stderr, "error: preview method %s\n", - preview); + LOG_ERROR("error: preview method %s", preview); return -1; } preview_method = (preview_t)preview_found; @@ -161,7 +159,7 @@ struct SDCliParams { bool process_and_check() { if (output_path.length() == 0) { - fprintf(stderr, "error: the following arguments are required: output_path\n"); + LOG_ERROR("error: the following arguments are required: output_path"); return false; } @@ -219,18 +217,6 @@ void parse_args(int argc, const char** argv, SDCliParams& cli_params, SDContextP } } -static std::string sd_basename(const std::string& path) { - size_t pos = path.find_last_of('/'); - if (pos != std::string::npos) { - return path.substr(pos + 1); - } - pos = path.find_last_of('\\'); - if (pos != std::string::npos) { - return path.substr(pos + 1); - } - return path; -} - std::string get_image_params(const SDCliParams& cli_params, const SDContextParams& ctx_params, const SDGenerationParams& gen_params, int64_t seed) { std::string parameter_string = gen_params.prompt_with_lora + "\n"; if (gen_params.negative_prompt.size() != 0) { @@ -288,47 +274,9 @@ std::string get_image_params(const SDCliParams& cli_params, const SDContextParam return parameter_string; } -/* Enables Printing the log level tag in color using ANSI escape codes */ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { SDCliParams* cli_params = (SDCliParams*)data; - int tag_color; - const char* level_str; - FILE* out_stream = (level == SD_LOG_ERROR) ? stderr : stdout; - - if (!log || (!cli_params->verbose && level <= SD_LOG_DEBUG)) { - return; - } - - switch (level) { - case SD_LOG_DEBUG: - tag_color = 37; - level_str = "DEBUG"; - break; - case SD_LOG_INFO: - tag_color = 34; - level_str = "INFO"; - break; - case SD_LOG_WARN: - tag_color = 35; - level_str = "WARN"; - break; - case SD_LOG_ERROR: - tag_color = 31; - level_str = "ERROR"; - break; - default: /* Potential future-proofing */ - tag_color = 33; - level_str = "?????"; - break; - } - - if (cli_params->color == true) { - fprintf(out_stream, "\033[%d;1m[%-5s]\033[0m ", tag_color, level_str); - } else { - fprintf(out_stream, "[%-5s] ", level_str); - } - fputs(log, out_stream); - fflush(out_stream); + log_print(level, log, cli_params->verbose, cli_params->color); } bool load_images_from_dir(const std::string dir, @@ -338,7 +286,7 @@ bool load_images_from_dir(const std::string dir, int max_image_num = 0, bool verbose = false) { if (!fs::exists(dir) || !fs::is_directory(dir)) { - fprintf(stderr, "'%s' is not a valid directory\n", dir.c_str()); + LOG_ERROR("'%s' is not a valid directory\n", dir.c_str()); return false; } @@ -360,14 +308,12 @@ bool load_images_from_dir(const std::string dir, std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp") { - if (verbose) { - printf("load image %zu from '%s'\n", images.size(), path.c_str()); - } + LOG_DEBUG("load image %zu from '%s'", images.size(), path.c_str()); int width = 0; int height = 0; uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height, expected_width, expected_height); if (image_buffer == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", path.c_str()); + LOG_ERROR("load image from '%s' failed", path.c_str()); return false; } @@ -429,6 +375,8 @@ int main(int argc, const char* argv[]) { cli_params.preview_fps /= 4; sd_set_log_callback(sd_log_cb, (void*)&cli_params); + log_verbose = cli_params.verbose; + log_color = cli_params.color; sd_set_preview_callback(step_callback, cli_params.preview_method, cli_params.preview_interval, @@ -437,10 +385,10 @@ int main(int argc, const char* argv[]) { (void*)&cli_params); if (cli_params.verbose) { - printf("%s", sd_get_system_info()); - printf("%s\n", cli_params.to_string().c_str()); - printf("%s\n", ctx_params.to_string().c_str()); - printf("%s\n", gen_params.to_string().c_str()); + LOG_INFO("%s", sd_get_system_info()); + LOG_INFO("%s", cli_params.to_string().c_str()); + LOG_INFO("%s", ctx_params.to_string().c_str()); + LOG_INFO("%s", gen_params.to_string().c_str()); } if (cli_params.mode == CONVERT) { @@ -450,17 +398,16 @@ int main(int argc, const char* argv[]) { ctx_params.wtype, ctx_params.tensor_type_rules.c_str()); if (!success) { - fprintf(stderr, - "convert '%s'/'%s' to '%s' failed\n", - ctx_params.model_path.c_str(), - ctx_params.vae_path.c_str(), - cli_params.output_path.c_str()); + LOG_ERROR("convert '%s'/'%s' to '%s' failed", + ctx_params.model_path.c_str(), + ctx_params.vae_path.c_str(), + cli_params.output_path.c_str()); return 1; } else { - printf("convert '%s'/'%s' to '%s' success\n", - ctx_params.model_path.c_str(), - ctx_params.vae_path.c_str(), - cli_params.output_path.c_str()); + LOG_INFO("convert '%s'/'%s' to '%s' success", + ctx_params.model_path.c_str(), + ctx_params.vae_path.c_str(), + cli_params.output_path.c_str()); return 0; } } @@ -503,7 +450,7 @@ int main(int argc, const char* argv[]) { int height = 0; init_image.data = load_image_from_file(gen_params.init_image_path.c_str(), width, height, gen_params.width, gen_params.height); if (init_image.data == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", gen_params.init_image_path.c_str()); + LOG_ERROR("load image from '%s' failed", gen_params.init_image_path.c_str()); release_all_resources(); return 1; } @@ -516,7 +463,7 @@ int main(int argc, const char* argv[]) { int height = 0; end_image.data = load_image_from_file(gen_params.end_image_path.c_str(), width, height, gen_params.width, gen_params.height); if (end_image.data == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", gen_params.end_image_path.c_str()); + LOG_ERROR("load image from '%s' failed", gen_params.end_image_path.c_str()); release_all_resources(); return 1; } @@ -528,7 +475,7 @@ int main(int argc, const char* argv[]) { int height = 0; mask_image.data = load_image_from_file(gen_params.mask_image_path.c_str(), width, height, gen_params.width, gen_params.height, 1); if (mask_image.data == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", gen_params.mask_image_path.c_str()); + LOG_ERROR("load image from '%s' failed", gen_params.mask_image_path.c_str()); release_all_resources(); return 1; } @@ -536,7 +483,7 @@ int main(int argc, const char* argv[]) { mask_image.data = (uint8_t*)malloc(gen_params.width * gen_params.height); memset(mask_image.data, 255, gen_params.width * gen_params.height); if (mask_image.data == nullptr) { - fprintf(stderr, "malloc mask image failed\n"); + LOG_ERROR("malloc mask image failed"); release_all_resources(); return 1; } @@ -547,7 +494,7 @@ int main(int argc, const char* argv[]) { int height = 0; control_image.data = load_image_from_file(gen_params.control_image_path.c_str(), width, height, gen_params.width, gen_params.height); if (control_image.data == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", gen_params.control_image_path.c_str()); + LOG_ERROR("load image from '%s' failed", gen_params.control_image_path.c_str()); release_all_resources(); return 1; } @@ -568,7 +515,7 @@ int main(int argc, const char* argv[]) { int height = 0; uint8_t* image_buffer = load_image_from_file(path.c_str(), width, height); if (image_buffer == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", path.c_str()); + LOG_ERROR("load image from '%s' failed", path.c_str()); release_all_resources(); return 1; } @@ -616,7 +563,7 @@ int main(int argc, const char* argv[]) { num_results = 1; results = (sd_image_t*)calloc(num_results, sizeof(sd_image_t)); if (results == nullptr) { - printf("failed to allocate results array\n"); + LOG_INFO("failed to allocate results array"); release_all_resources(); return 1; } @@ -627,7 +574,7 @@ int main(int argc, const char* argv[]) { sd_ctx_t* sd_ctx = new_sd_ctx(&sd_ctx_params); if (sd_ctx == nullptr) { - printf("new_sd_ctx_t failed\n"); + LOG_INFO("new_sd_ctx_t failed"); release_all_resources(); return 1; } @@ -704,7 +651,7 @@ int main(int argc, const char* argv[]) { } if (results == nullptr) { - printf("generate failed\n"); + LOG_ERROR("generate failed"); free_sd_ctx(sd_ctx); return 1; } @@ -721,7 +668,7 @@ int main(int argc, const char* argv[]) { gen_params.upscale_tile_size); if (upscaler_ctx == nullptr) { - printf("new_upscaler_ctx failed\n"); + LOG_ERROR("new_upscaler_ctx failed"); } else { for (int i = 0; i < num_results; i++) { if (results[i].data == nullptr) { @@ -731,7 +678,7 @@ int main(int argc, const char* argv[]) { for (int u = 0; u < gen_params.upscale_repeats; ++u) { sd_image_t upscaled_image = upscale(upscaler_ctx, current_image, upscale_factor); if (upscaled_image.data == nullptr) { - printf("upscale failed\n"); + LOG_ERROR("upscale failed"); break; } free(current_image.data); @@ -749,8 +696,8 @@ int main(int argc, const char* argv[]) { std::error_code ec; fs::create_directories(out_dir, ec); // OK if already exists if (ec) { - fprintf(stderr, "failed to create directory '%s': %s\n", - out_dir.string().c_str(), ec.message().c_str()); + LOG_ERROR("failed to create directory '%s': %s", + out_dir.string().c_str(), ec.message().c_str()); return 1; } } @@ -780,7 +727,7 @@ int main(int argc, const char* argv[]) { vid_output_path = base_path + ".avi"; } create_mjpg_avi_from_sd_images(vid_output_path.c_str(), results, num_results, gen_params.fps); - printf("save result MJPG AVI video to '%s'\n", vid_output_path.c_str()); + LOG_INFO("save result MJPG AVI video to '%s'\n", vid_output_path.c_str()); } else { // appending ".png" to absent or unknown extension if (!is_jpg && file_ext_lower != ".png") { @@ -796,11 +743,11 @@ int main(int argc, const char* argv[]) { if (is_jpg) { write_ok = stbi_write_jpg(final_image_path.c_str(), results[i].width, results[i].height, results[i].channel, results[i].data, 90, get_image_params(cli_params, ctx_params, gen_params, gen_params.seed + i).c_str()); - printf("save result JPEG image to '%s' (%s)\n", final_image_path.c_str(), write_ok == 0 ? "failure" : "success"); + LOG_INFO("save result JPEG image to '%s' (%s)", final_image_path.c_str(), write_ok == 0 ? "failure" : "success"); } else { write_ok = stbi_write_png(final_image_path.c_str(), results[i].width, results[i].height, results[i].channel, results[i].data, 0, get_image_params(cli_params, ctx_params, gen_params, gen_params.seed + i).c_str()); - printf("save result PNG image to '%s' (%s)\n", final_image_path.c_str(), write_ok == 0 ? "failure" : "success"); + LOG_INFO("save result PNG image to '%s' (%s)", final_image_path.c_str(), write_ok == 0 ? "failure" : "success"); } } } diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 2d890af8a..f3a561367 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -86,6 +86,114 @@ static std::string argv_to_utf8(int index, const char** argv) { #endif +static void print_utf8(FILE* stream, const char* utf8) { + if (!utf8) + return; + +#ifdef _WIN32 + HANDLE h = (stream == stderr) + ? GetStdHandle(STD_ERROR_HANDLE) + : GetStdHandle(STD_OUTPUT_HANDLE); + + int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); + if (wlen <= 0) + return; + + wchar_t* wbuf = (wchar_t*)malloc(wlen * sizeof(wchar_t)); + MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wbuf, wlen); + + DWORD written; + WriteConsoleW(h, wbuf, wlen - 1, &written, NULL); + + free(wbuf); +#else + fputs(utf8, stream); +#endif +} + +static std::string sd_basename(const std::string& path) { + size_t pos = path.find_last_of('/'); + if (pos != std::string::npos) { + return path.substr(pos + 1); + } + pos = path.find_last_of('\\'); + if (pos != std::string::npos) { + return path.substr(pos + 1); + } + return path; +} + +static void log_print(enum sd_log_level_t level, const char* log, bool verbose, bool color) { + int tag_color; + const char* level_str; + FILE* out_stream = (level == SD_LOG_ERROR) ? stderr : stdout; + + if (!log || (!verbose && level <= SD_LOG_DEBUG)) { + return; + } + + switch (level) { + case SD_LOG_DEBUG: + tag_color = 37; + level_str = "DEBUG"; + break; + case SD_LOG_INFO: + tag_color = 34; + level_str = "INFO"; + break; + case SD_LOG_WARN: + tag_color = 35; + level_str = "WARN"; + break; + case SD_LOG_ERROR: + tag_color = 31; + level_str = "ERROR"; + break; + default: /* Potential future-proofing */ + tag_color = 33; + level_str = "?????"; + break; + } + + if (color) { + fprintf(out_stream, "\033[%d;1m[%-5s]\033[0m ", tag_color, level_str); + } else { + fprintf(out_stream, "[%-5s] ", level_str); + } + print_utf8(out_stream, log); + fflush(out_stream); +} + +#define LOG_BUFFER_SIZE 4096 + +static bool log_verbose = false; +static bool log_color = false; + +static void log_printf(sd_log_level_t level, const char* file, int line, const char* format, ...) { + va_list args; + va_start(args, format); + + static char log_buffer[LOG_BUFFER_SIZE + 1]; + int written = snprintf(log_buffer, LOG_BUFFER_SIZE, "%s:%-4d - ", sd_basename(file).c_str(), line); + + if (written >= 0 && written < LOG_BUFFER_SIZE) { + vsnprintf(log_buffer + written, LOG_BUFFER_SIZE - written, format, args); + } + size_t len = strlen(log_buffer); + if (log_buffer[len - 1] != '\n') { + strncat(log_buffer, "\n", LOG_BUFFER_SIZE - len); + } + + log_print(level, log_buffer, log_verbose, log_color); + + va_end(args); +} + +#define LOG_DEBUG(format, ...) log_printf(SD_LOG_DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__) +#define LOG_INFO(format, ...) log_printf(SD_LOG_INFO, __FILE__, __LINE__, format, ##__VA_ARGS__) +#define LOG_WARN(format, ...) log_printf(SD_LOG_WARN, __FILE__, __LINE__, format, ##__VA_ARGS__) +#define LOG_ERROR(format, ...) log_printf(SD_LOG_ERROR, __FILE__, __LINE__, format, ##__VA_ARGS__) + struct StringOption { std::string short_name; std::string long_name; @@ -295,11 +403,11 @@ static bool parse_options(int argc, const char** argv, const std::vector 1.f) { - fprintf(stderr, "error: can only work with strength in [0.0, 1.0]\n"); + LOG_ERROR("error: can only work with strength in [0.0, 1.0]\n"); return false; } @@ -1523,31 +1631,31 @@ struct SDGenerationParams { }; trim(token); if (token.empty()) { - fprintf(stderr, "error: invalid easycache option '%s'\n", easycache_option.c_str()); + LOG_ERROR("error: invalid easycache option '%s'", easycache_option.c_str()); return false; } if (idx >= 3) { - fprintf(stderr, "error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); + LOG_ERROR("error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); return false; } try { values[idx] = std::stof(token); } catch (const std::exception&) { - fprintf(stderr, "error: invalid easycache value '%s'\n", token.c_str()); + LOG_ERROR("error: invalid easycache value '%s'", token.c_str()); return false; } idx++; } if (idx != 3) { - fprintf(stderr, "error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); + LOG_ERROR("error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); return false; } if (values[0] < 0.0f) { - fprintf(stderr, "error: easycache threshold must be non-negative\n"); + LOG_ERROR("error: easycache threshold must be non-negative\n"); return false; } if (values[1] < 0.0f || values[1] >= 1.0f || values[2] <= 0.0f || values[2] > 1.0f || values[1] >= values[2]) { - fprintf(stderr, "error: easycache start/end percents must satisfy 0.0 <= start < end <= 1.0\n"); + LOG_ERROR("error: easycache start/end percents must satisfy 0.0 <= start < end <= 1.0\n"); return false; } easycache_params.enabled = true; @@ -1587,7 +1695,7 @@ struct SDGenerationParams { if (mode == UPSCALE) { if (init_image_path.length() == 0) { - fprintf(stderr, "error: upscale mode needs an init image (--init-img)\n"); + LOG_ERROR("error: upscale mode needs an init image (--init-img)\n"); return false; } } @@ -1702,13 +1810,13 @@ uint8_t* load_image_common(bool from_memory, image_buffer = (uint8_t*)stbi_load(image_path_or_bytes, &width, &height, &c, expected_channel); } if (image_buffer == nullptr) { - fprintf(stderr, "load image from '%s' failed\n", image_path); + LOG_ERROR("load image from '%s' failed", image_path); return nullptr; } if (c < expected_channel) { fprintf(stderr, "the number of channels for the input image must be >= %d," - "but got %d channels, image_path = %s\n", + "but got %d channels, image_path = %s", expected_channel, c, image_path); @@ -1716,12 +1824,12 @@ uint8_t* load_image_common(bool from_memory, return nullptr; } if (width <= 0) { - fprintf(stderr, "error: the width of image must be greater than 0, image_path = %s\n", image_path); + LOG_ERROR("error: the width of image must be greater than 0, image_path = %s", image_path); free(image_buffer); return nullptr; } if (height <= 0) { - fprintf(stderr, "error: the height of image must be greater than 0, image_path = %s\n", image_path); + LOG_ERROR("error: the height of image must be greater than 0, image_path = %s", image_path); free(image_buffer); return nullptr; } @@ -1743,10 +1851,10 @@ uint8_t* load_image_common(bool from_memory, } if (crop_x != 0 || crop_y != 0) { - printf("crop input image from %dx%d to %dx%d, image_path = %s\n", width, height, crop_w, crop_h, image_path); + LOG_INFO("crop input image from %dx%d to %dx%d, image_path = %s", width, height, crop_w, crop_h, image_path); uint8_t* cropped_image_buffer = (uint8_t*)malloc(crop_w * crop_h * expected_channel); if (cropped_image_buffer == nullptr) { - fprintf(stderr, "error: allocate memory for crop\n"); + LOG_ERROR("error: allocate memory for crop\n"); free(image_buffer); return nullptr; } @@ -1762,13 +1870,13 @@ uint8_t* load_image_common(bool from_memory, image_buffer = cropped_image_buffer; } - printf("resize input image from %dx%d to %dx%d\n", width, height, expected_width, expected_height); + LOG_INFO("resize input image from %dx%d to %dx%d", width, height, expected_width, expected_height); int resized_height = expected_height; int resized_width = expected_width; uint8_t* resized_image_buffer = (uint8_t*)malloc(resized_height * resized_width * expected_channel); if (resized_image_buffer == nullptr) { - fprintf(stderr, "error: allocate memory for resize input image\n"); + LOG_ERROR("error: allocate memory for resize input image\n"); free(image_buffer); return nullptr; } diff --git a/examples/server/main.cpp b/examples/server/main.cpp index f1ba0cd14..b06897611 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -151,12 +151,12 @@ struct SDSvrParams { bool process_and_check() { if (listen_ip.empty()) { - fprintf(stderr, "error: the following arguments are required: listen_ip\n"); + LOG_ERROR("error: the following arguments are required: listen_ip"); return false; } if (listen_port < 0 || listen_port > 65535) { - fprintf(stderr, "error: listen_port should be in the range [0, 65535]\n"); + LOG_ERROR("error: listen_port should be in the range [0, 65535]"); return false; } return true; @@ -256,47 +256,9 @@ std::vector write_image_to_vector( return buffer; } -/* Enables Printing the log level tag in color using ANSI escape codes */ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { SDSvrParams* svr_params = (SDSvrParams*)data; - int tag_color; - const char* level_str; - FILE* out_stream = (level == SD_LOG_ERROR) ? stderr : stdout; - - if (!log || (!svr_params->verbose && level <= SD_LOG_DEBUG)) { - return; - } - - switch (level) { - case SD_LOG_DEBUG: - tag_color = 37; - level_str = "DEBUG"; - break; - case SD_LOG_INFO: - tag_color = 34; - level_str = "INFO"; - break; - case SD_LOG_WARN: - tag_color = 35; - level_str = "WARN"; - break; - case SD_LOG_ERROR: - tag_color = 31; - level_str = "ERROR"; - break; - default: /* Potential future-proofing */ - tag_color = 33; - level_str = "?????"; - break; - } - - if (svr_params->color == true) { - fprintf(out_stream, "\033[%d;1m[%-5s]\033[0m ", tag_color, level_str); - } else { - fprintf(out_stream, "[%-5s] ", level_str); - } - fputs(log, out_stream); - fflush(out_stream); + log_print(level, log, svr_params->verbose, svr_params->color); } int main(int argc, const char** argv) { @@ -306,19 +268,21 @@ int main(int argc, const char** argv) { parse_args(argc, argv, svr_params, ctx_params, default_gen_params); sd_set_log_callback(sd_log_cb, (void*)&svr_params); + log_verbose = svr_params.verbose; + log_color = svr_params.color; if (svr_params.verbose) { - printf("%s", sd_get_system_info()); - printf("%s\n", svr_params.to_string().c_str()); - printf("%s\n", ctx_params.to_string().c_str()); - printf("%s\n", default_gen_params.to_string().c_str()); + LOG_INFO("%s", sd_get_system_info()); + LOG_INFO("%s", svr_params.to_string().c_str()); + LOG_INFO("%s", ctx_params.to_string().c_str()); + LOG_INFO("%s", default_gen_params.to_string().c_str()); } sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false, false, false); sd_ctx_t* sd_ctx = new_sd_ctx(&sd_ctx_params); if (sd_ctx == nullptr) { - printf("new_sd_ctx_t failed\n"); + LOG_ERROR("new_sd_ctx_t failed"); return 1; } @@ -431,9 +395,7 @@ int main(int argc, const char** argv) { return; } - if (svr_params.verbose) { - printf("%s\n", gen_params.to_string().c_str()); - } + LOG_DEBUG("%s\n", gen_params.to_string().c_str()); sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; @@ -490,7 +452,7 @@ int main(int argc, const char** argv) { results[i].channel, output_compression); if (image_bytes.empty()) { - printf("write image to mem failed\n"); + LOG_ERROR("write image to mem failed"); continue; } @@ -611,9 +573,7 @@ int main(int argc, const char** argv) { return; } - if (svr_params.verbose) { - printf("%s\n", gen_params.to_string().c_str()); - } + LOG_DEBUG("%s\n", gen_params.to_string().c_str()); sd_image_t init_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; sd_image_t control_image = {(uint32_t)gen_params.width, (uint32_t)gen_params.height, 3, nullptr}; @@ -735,7 +695,7 @@ int main(int argc, const char** argv) { } }); - printf("listening on: %s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port); + LOG_INFO("listening on: %s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port); svr.listen(svr_params.listen_ip, svr_params.listen_port); // cleanup From c3ad6a13e10a1358f48dad4d2ce8268a7c0781ae Mon Sep 17 00:00:00 2001 From: leejet Date: Tue, 16 Dec 2025 23:11:27 +0800 Subject: [PATCH 22/49] refactor: optimize the printing of version log (#1102) --- examples/cli/main.cpp | 19 +++++-------------- examples/server/main.cpp | 15 +++++++++------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 46260b173..42b909e4f 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -31,7 +31,6 @@ struct SDCliParams { std::string output_path = "output.png"; bool verbose = false; - bool version = false; bool canny_preprocess = false; preview_t preview_method = PREVIEW_NONE; @@ -74,10 +73,6 @@ struct SDCliParams { "--verbose", "print extra info", true, &verbose}, - {"", - "--version", - "print stable-diffusion.cpp version", - true, &version}, {"", "--color", "colors the logging tags according to level", @@ -354,9 +349,6 @@ int main(int argc, const char* argv[]) { SDGenerationParams gen_params; parse_args(argc, argv, cli_params, ctx_params, gen_params); - if (cli_params.verbose || cli_params.version) { - std::cout << version_string() << "\n"; - } if (gen_params.video_frames > 4) { size_t last_dot_pos = cli_params.preview_path.find_last_of("."); std::string base_path = cli_params.preview_path; @@ -384,12 +376,11 @@ int main(int argc, const char* argv[]) { cli_params.preview_noisy, (void*)&cli_params); - if (cli_params.verbose) { - LOG_INFO("%s", sd_get_system_info()); - LOG_INFO("%s", cli_params.to_string().c_str()); - LOG_INFO("%s", ctx_params.to_string().c_str()); - LOG_INFO("%s", gen_params.to_string().c_str()); - } + LOG_DEBUG("version: %s", version_string().c_str()); + LOG_DEBUG("%s", sd_get_system_info()); + LOG_DEBUG("%s", cli_params.to_string().c_str()); + LOG_DEBUG("%s", ctx_params.to_string().c_str()); + LOG_DEBUG("%s", gen_params.to_string().c_str()); if (cli_params.mode == CONVERT) { bool success = convert(ctx_params.model_path.c_str(), diff --git a/examples/server/main.cpp b/examples/server/main.cpp index b06897611..39359fbbe 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -262,6 +262,10 @@ void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { } int main(int argc, const char** argv) { + if (argc > 1 && std::string(argv[1]) == "--version") { + std::cout << version_string() << "\n"; + return EXIT_SUCCESS; + } SDSvrParams svr_params; SDContextParams ctx_params; SDGenerationParams default_gen_params; @@ -271,12 +275,11 @@ int main(int argc, const char** argv) { log_verbose = svr_params.verbose; log_color = svr_params.color; - if (svr_params.verbose) { - LOG_INFO("%s", sd_get_system_info()); - LOG_INFO("%s", svr_params.to_string().c_str()); - LOG_INFO("%s", ctx_params.to_string().c_str()); - LOG_INFO("%s", default_gen_params.to_string().c_str()); - } + LOG_DEBUG("version: %s", version_string().c_str()); + LOG_DEBUG("%s", sd_get_system_info()); + LOG_DEBUG("%s", svr_params.to_string().c_str()); + LOG_DEBUG("%s", ctx_params.to_string().c_str()); + LOG_DEBUG("%s", default_gen_params.to_string().c_str()); sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false, false, false); sd_ctx_t* sd_ctx = new_sd_ctx(&sd_ctx_params); From c2e18c86e863b106af0ca8996430639c0eab6b62 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 17 Dec 2025 23:28:59 +0800 Subject: [PATCH 23/49] fix: make flash attn work with high noise diffusion model (#1111) --- stable-diffusion.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 44bd3ccac..d57702158 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -537,6 +537,9 @@ class StableDiffusionGGML { if (sd_ctx_params->diffusion_flash_attn) { LOG_INFO("Using flash attention in the diffusion model"); diffusion_model->set_flash_attn_enabled(true); + if (high_noise_diffusion_model) { + high_noise_diffusion_model->set_flash_attn_enabled(true); + } } cond_stage_model->alloc_params_buffer(); @@ -3723,6 +3726,9 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s init_latent = sd_ctx->sd->generate_init_latent(work_ctx, width, height, frames, true); } + print_ggml_tensor(init_latent, true); + print_ggml_tensor(concat_latent, true); + // Get learned condition ConditionerParams condition_params; condition_params.clip_skip = sd_vid_gen_params->clip_skip; From bda7fab9f208dff4b67179a68f694b6ddec13326 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 17 Dec 2025 23:43:37 +0800 Subject: [PATCH 24/49] chore: remove unused debug code --- stable-diffusion.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index d57702158..e3f49aacf 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -3726,9 +3726,6 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s init_latent = sd_ctx->sd->generate_init_latent(work_ctx, width, height, frames, true); } - print_ggml_tensor(init_latent, true); - print_ggml_tensor(concat_latent, true); - // Get learned condition ConditionerParams condition_params; condition_params.clip_skip = sd_vid_gen_params->clip_skip; From 97cf2efe45e37c00006e19e2cbdec372a5186a79 Mon Sep 17 00:00:00 2001 From: Daniele <57776841+daniandtheweb@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:02:55 +0100 Subject: [PATCH 25/49] feat: add KL Optimal scheduler (#1098) --- denoiser.hpp | 39 ++++++++++++++++++++++++++++++++++++++ examples/cli/README.md | 2 +- examples/common/common.hpp | 4 ++-- examples/server/README.md | 4 ++-- stable-diffusion.cpp | 3 ++- stable-diffusion.h | 1 + 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/denoiser.hpp b/denoiser.hpp index 32f402786..fc5230d7b 100644 --- a/denoiser.hpp +++ b/denoiser.hpp @@ -347,6 +347,41 @@ struct SmoothStepScheduler : SigmaScheduler { } }; +// Implementation adapted from https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15608 +struct KLOptimalScheduler : SigmaScheduler { + std::vector get_sigmas(uint32_t n, float sigma_min, float sigma_max, t_to_sigma_t t_to_sigma) override { + std::vector sigmas; + + if (n == 0) { + return sigmas; + } + if (n == 1) { + sigmas.push_back(sigma_max); + sigmas.push_back(0.0f); + return sigmas; + } + + float alpha_min = std::atan(sigma_min); + float alpha_max = std::atan(sigma_max); + + for (uint32_t i = 0; i < n; ++i) { + // t goes from 0.0 to 1.0 + float t = static_cast(i) / static_cast(n-1); + + // Interpolate in the angle domain + float angle = t * alpha_min + (1.0f - t) * alpha_max; + + // Convert back to sigma + sigmas.push_back(std::tan(angle)); + } + + // Append the final zero to sigma + sigmas.push_back(0.0f); + + return sigmas; + } +}; + struct Denoiser { virtual float sigma_min() = 0; virtual float sigma_max() = 0; @@ -392,6 +427,10 @@ struct Denoiser { LOG_INFO("get_sigmas with SmoothStep scheduler"); scheduler = std::make_shared(); break; + case KL_OPTIMAL_SCHEDULER: + LOG_INFO("get_sigmas with KL Optimal scheduler"); + scheduler = std::make_shared(); + break; case LCM_SCHEDULER: LOG_INFO("get_sigmas with LCM scheduler"); scheduler = std::make_shared(); diff --git a/examples/cli/README.md b/examples/cli/README.md index 8531b2aed..da7734d31 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -120,7 +120,7 @@ Generation Options: tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise) --high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise - --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], + --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, kl_optimal, lcm], default: discrete --sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0"). --skip-layers layers to skip for SLG steps (default: [7,8,9]) diff --git a/examples/common/common.hpp b/examples/common/common.hpp index f3a561367..b81dab784 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -1409,7 +1409,7 @@ struct SDGenerationParams { on_high_noise_sample_method_arg}, {"", "--scheduler", - "denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], default: discrete", + "denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, kl_optimal, lcm], default: discrete", on_scheduler_arg}, {"", "--sigmas", @@ -1911,4 +1911,4 @@ uint8_t* load_image_from_memory(const char* image_bytes, int expected_height = 0, int expected_channel = 3) { return load_image_common(true, image_bytes, len, width, height, expected_width, expected_height, expected_channel); -} \ No newline at end of file +} diff --git a/examples/server/README.md b/examples/server/README.md index a475856fa..ae1049680 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -114,11 +114,11 @@ Default Generation Options: tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise) --high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise - --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, lcm], + --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, kl_optimal, lcm], default: discrete --sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0"). --skip-layers layers to skip for SLG steps (default: [7,8,9]) --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) -r, --ref-image reference image for Flux Kontext models (can be used multiple times) --easycache enable EasyCache for DiT models with optional "threshold,start_percent,end_percent" (default: 0.2,0.15,0.95) -``` \ No newline at end of file +``` diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index e3f49aacf..2c77a1374 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -2412,6 +2412,7 @@ const char* scheduler_to_str[] = { "sgm_uniform", "simple", "smoothstep", + "kl_optimal", "lcm", }; @@ -3888,4 +3889,4 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s LOG_INFO("generate_video completed in %.2fs", (t5 - t0) * 1.0f / 1000); return result_images; -} \ No newline at end of file +} diff --git a/stable-diffusion.h b/stable-diffusion.h index 9266ba437..110120e97 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -60,6 +60,7 @@ enum scheduler_t { SGM_UNIFORM_SCHEDULER, SIMPLE_SCHEDULER, SMOOTHSTEP_SCHEDULER, + KL_OPTIMAL_SCHEDULER, LCM_SCHEDULER, SCHEDULER_COUNT }; From 78e15bd4af2755762acf89654c79e0bfc3b2adab Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Thu, 18 Dec 2025 10:43:39 -0300 Subject: [PATCH 26/49] feat: default to LCM scheduler for LCM sampling (#1109) * feat: default to LCM scheduler for LCM sampling * fix bug and attempt to get default scheduler for vid_gen when none is set --------- Co-authored-by: leejet --- examples/cli/main.cpp | 4 ++-- stable-diffusion.cpp | 17 ++++++++++++++--- stable-diffusion.h | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 42b909e4f..a1b92f77e 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -579,7 +579,7 @@ int main(int argc, const char* argv[]) { } if (gen_params.sample_params.scheduler == SCHEDULER_COUNT) { - gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx); + gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx, gen_params.sample_params.sample_method); } if (cli_params.mode == IMG_GEN) { @@ -752,4 +752,4 @@ int main(int argc, const char* argv[]) { release_all_resources(); return 0; -} \ No newline at end of file +} diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 2c77a1374..4485ac7a7 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -2777,13 +2777,16 @@ enum sample_method_t sd_get_default_sample_method(const sd_ctx_t* sd_ctx) { return EULER_A_SAMPLE_METHOD; } -enum scheduler_t sd_get_default_scheduler(const sd_ctx_t* sd_ctx) { +enum scheduler_t sd_get_default_scheduler(const sd_ctx_t* sd_ctx, enum sample_method_t sample_method) { if (sd_ctx != nullptr && sd_ctx->sd != nullptr) { auto edm_v_denoiser = std::dynamic_pointer_cast(sd_ctx->sd->denoiser); if (edm_v_denoiser) { return EXPONENTIAL_SCHEDULER; } } + if (sample_method == LCM_SAMPLE_METHOD) { + return LCM_SCHEDULER; + } return DISCRETE_SCHEDULER; } @@ -3218,9 +3221,13 @@ sd_image_t* generate_image(sd_ctx_t* sd_ctx, const sd_img_gen_params_t* sd_img_g LOG_WARN("sample_steps != custom_sigmas_count - 1, set sample_steps to %d", sample_steps); } } else { + scheduler_t scheduler = sd_img_gen_params->sample_params.scheduler; + if (scheduler == SCHEDULER_COUNT) { + scheduler = sd_get_default_scheduler(sd_ctx, sample_method); + } sigmas = sd_ctx->sd->denoiser->get_sigmas(sample_steps, sd_ctx->sd->get_image_seq_len(height, width), - sd_img_gen_params->sample_params.scheduler, + scheduler, sd_ctx->sd->version); } @@ -3503,9 +3510,13 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s } } } else { + scheduler_t scheduler = sd_vid_gen_params->sample_params.scheduler; + if (scheduler == SCHEDULER_COUNT) { + scheduler = sd_get_default_scheduler(sd_ctx, sample_method); + } sigmas = sd_ctx->sd->denoiser->get_sigmas(total_steps, 0, - sd_vid_gen_params->sample_params.scheduler, + scheduler, sd_ctx->sd->version); } diff --git a/stable-diffusion.h b/stable-diffusion.h index 110120e97..adb65a1d2 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -335,7 +335,7 @@ SD_API void sd_sample_params_init(sd_sample_params_t* sample_params); SD_API char* sd_sample_params_to_str(const sd_sample_params_t* sample_params); SD_API enum sample_method_t sd_get_default_sample_method(const sd_ctx_t* sd_ctx); -SD_API enum scheduler_t sd_get_default_scheduler(const sd_ctx_t* sd_ctx); +SD_API enum scheduler_t sd_get_default_scheduler(const sd_ctx_t* sd_ctx, enum sample_method_t sample_method); SD_API void sd_img_gen_params_init(sd_img_gen_params_t* sd_img_gen_params); SD_API char* sd_img_gen_params_to_str(const sd_img_gen_params_t* sd_img_gen_params); From 8e9f3a4d9e4539d58c56cd523956106222d56713 Mon Sep 17 00:00:00 2001 From: leejet Date: Thu, 18 Dec 2025 21:44:16 +0800 Subject: [PATCH 27/49] feat: add support for underline style lora of flux (#1103) * feat: add support for underline style lora of flux * add support for underline style lora of t5 * add more protected tokens --- name_conversion.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/name_conversion.cpp b/name_conversion.cpp index 8b521486d..6a8ae72c0 100644 --- a/name_conversion.cpp +++ b/name_conversion.cpp @@ -835,6 +835,7 @@ std::string convert_sep_to_dot(std::string name) { "proj_out", "transformer_blocks", "single_transformer_blocks", + "single_blocks", "diffusion_model", "cond_stage_model", "first_stage_model", @@ -876,7 +877,18 @@ std::string convert_sep_to_dot(std::string name) { "ff_context", "norm_added_q", "norm_added_v", - "to_add_out"}; + "to_add_out", + "txt_mod", + "img_mod", + "txt_mlp", + "img_mlp", + "proj_mlp", + "wi_0", + "wi_1", + "norm1_context", + "ff_context", + "x_embedder", + }; // record the positions of underscores that should NOT be replaced std::unordered_set protected_positions; @@ -1020,12 +1032,14 @@ std::string convert_tensor_name(std::string name, SDVersion version) { } } - if (sd_version_is_unet(version) || is_lycoris_underline) { + // LOG_DEBUG("name %s %d", name.c_str(), version); + + if (sd_version_is_unet(version) || sd_version_is_flux(version) || is_lycoris_underline) { name = convert_sep_to_dot(name); } } - std::vector> prefix_map = { + std::unordered_map prefix_map = { {"diffusion_model.", "model.diffusion_model."}, {"unet.", "model.diffusion_model."}, {"transformer.", "model.diffusion_model."}, // dit @@ -1040,8 +1054,13 @@ std::string convert_tensor_name(std::string name, SDVersion version) { // {"te2.text_model.encoder.layers.", "cond_stage_model.1.model.transformer.resblocks."}, {"te2.", "cond_stage_model.1.transformer."}, {"te1.", "cond_stage_model.transformer."}, + {"te3.", "text_encoders.t5xxl.transformer."}, }; + if (sd_version_is_flux(version)) { + prefix_map["te1."] = "text_encoders.clip_l.transformer."; + } + replace_with_prefix_map(name, prefix_map); // diffusion model From 1f77545cf80d93a35b691b2327247e472371b6ea Mon Sep 17 00:00:00 2001 From: Weiqi Gao Date: Fri, 19 Dec 2025 23:31:09 +0800 Subject: [PATCH 28/49] docs: document usage of tae for VRAM reduction using wan (#1108) --- docs/taesd.md | 24 +++++++++++++++++++++++- docs/wan.md | 3 +++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/taesd.md b/docs/taesd.md index 5160b7934..a41c64d43 100644 --- a/docs/taesd.md +++ b/docs/taesd.md @@ -14,4 +14,26 @@ curl -L -O https://huggingface.co/madebyollin/taesd/resolve/main/diffusion_pytor ```bash sd-cli -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --taesd ../models/diffusion_pytorch_model.safetensors -``` \ No newline at end of file +``` + +### Qwen-Image and wan (TAEHV) + +sd.cpp also supports [TAEHV](https://github.com/madebyollin/taehv) (#937), which can be used for Qwen-Image and wan. + +- For **Qwen-Image and wan2.1 and wan2.2-A14B**, download the wan2.1 tae [safetensors weights](https://github.com/madebyollin/taehv/blob/main/safetensors/taew2_1.safetensors) + + Or curl + + ```bash + curl -L -O https://github.com/madebyollin/taehv/raw/refs/heads/main/safetensors/taew2_1.safetensors + ``` + +- For **wan2.2-TI2V-5B**, use the wan2.2 tae [safetensors weights](https://github.com/madebyollin/taehv/blob/main/safetensors/taew2_2.safetensors) + + Or curl + + ```bash + curl -L -O https://github.com/madebyollin/taehv/raw/refs/heads/main/safetensors/taew2_2.safetensors + ``` + +Then simply replace the `--vae xxx.safetensors` with `--tae xxx.safetensors` in the commands. If it still out of VRAM, add `--vae-conv-direct` to your command though might be slower. diff --git a/docs/wan.md b/docs/wan.md index ce15ba589..6f5749c8c 100644 --- a/docs/wan.md +++ b/docs/wan.md @@ -39,6 +39,9 @@ - safetensors: https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/vae/wan_2.1_vae.safetensors - wan_2.2_vae (for Wan2.2 TI2V 5B only) - safetensors: https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/blob/main/split_files/vae/wan2.2_vae.safetensors + + > Wan models vae requires really much VRAM! If you do not have enough VRAM, please try tae instead, though the results may be poorer. For tae usage, please refer to [taesd](taesd.md) + - Download umt5_xxl - safetensors: https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/text_encoders/umt5_xxl_fp16.safetensors - gguf: https://huggingface.co/city96/umt5-xxl-encoder-gguf/tree/main From 7c88c4765ce6ef8447bc2ac5d30c73365f5b25a9 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Fri, 19 Dec 2025 12:41:52 -0300 Subject: [PATCH 29/49] chore: give feedback about cfg values smaller than 1 (#1088) --- stable-diffusion.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 4485ac7a7..6446c4129 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -1497,6 +1497,17 @@ class StableDiffusionGGML { std::vector skip_layers(guidance.slg.layers, guidance.slg.layers + guidance.slg.layer_count); float cfg_scale = guidance.txt_cfg; + if (cfg_scale < 1.f) { + if (cfg_scale == 0.f) { + // Diffusers follow the convention from the original paper + // (https://arxiv.org/abs/2207.12598v1), so many distilled model docs + // recommend 0 as guidance; warn the user that it'll disable prompt folowing + LOG_WARN("unconditioned mode, images won't follow the prompt (use cfg-scale=1 for distilled models)"); + } else { + LOG_WARN("cfg value out of expected range may produce unexpected results"); + } + } + float img_cfg_scale = std::isfinite(guidance.img_cfg) ? guidance.img_cfg : guidance.txt_cfg; float slg_scale = guidance.slg.scale; From 23fce0bd846879e6b7e710415ca7db404e6d2d83 Mon Sep 17 00:00:00 2001 From: stduhpf Date: Fri, 19 Dec 2025 17:55:57 +0100 Subject: [PATCH 30/49] feat: add support for Chroma Radiance x0 (#1091) * Add x0 Flux pred (+prepare for others) * Fix convert models with empty tensors * patch_32 exp support attempt * improve support for patch_32 * follow official pipeline --------- Co-authored-by: leejet --- flux.hpp | 33 ++++++++++++++++++++++++++++++++- model.cpp | 7 +++++++ stable-diffusion.cpp | 3 +++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/flux.hpp b/flux.hpp index 1df2874ae..7ce263569 100644 --- a/flux.hpp +++ b/flux.hpp @@ -744,6 +744,8 @@ namespace Flux { int64_t nerf_mlp_ratio = 4; int64_t nerf_depth = 4; int64_t nerf_max_freqs = 8; + bool use_x0 = false; + bool use_patch_size_32 = false; }; struct FluxParams { @@ -781,7 +783,7 @@ namespace Flux { Flux(FluxParams params) : params(params) { if (params.version == VERSION_CHROMA_RADIANCE) { - std::pair kernel_size = {(int)params.patch_size, (int)params.patch_size}; + std::pair kernel_size = {16, 16}; std::pair stride = kernel_size; blocks["img_in_patch"] = std::make_shared(params.in_channels, @@ -1044,6 +1046,15 @@ namespace Flux { return img; } + struct ggml_tensor* _apply_x0_residual(GGMLRunnerContext* ctx, + struct ggml_tensor* predicted, + struct ggml_tensor* noisy, + struct ggml_tensor* timesteps) { + auto x = ggml_sub(ctx->ggml_ctx, noisy, predicted); + x = ggml_div(ctx->ggml_ctx, x, timesteps); + return x; + } + struct ggml_tensor* forward_chroma_radiance(GGMLRunnerContext* ctx, struct ggml_tensor* x, struct ggml_tensor* timestep, @@ -1068,6 +1079,13 @@ namespace Flux { auto img = pad_to_patch_size(ctx->ggml_ctx, x); auto orig_img = img; + if (params.chroma_radiance_params.use_patch_size_32) { + // It's supposed to be using GGML_SCALE_MODE_NEAREST, but this seems more stable + // Maybe the implementation of nearest-neighbor interpolation in ggml behaves differently than the one in PyTorch? + // img = F.interpolate(img, size=(H//2, W//2), mode="nearest") + img = ggml_interpolate(ctx->ggml_ctx, img, W / 2, H / 2, C, x->ne[3], GGML_SCALE_MODE_BILINEAR); + } + auto img_in_patch = std::dynamic_pointer_cast(blocks["img_in_patch"]); img = img_in_patch->forward(ctx, img); // [N, hidden_size, H/patch_size, W/patch_size] @@ -1104,6 +1122,10 @@ namespace Flux { out = nerf_final_layer_conv->forward(ctx, img_dct); // [N, C, H, W] + if (params.chroma_radiance_params.use_x0) { + out = _apply_x0_residual(ctx, out, orig_img, timestep); + } + return out; } @@ -1290,6 +1312,15 @@ namespace Flux { // not schnell flux_params.guidance_embed = true; } + if (tensor_name.find("__x0__") != std::string::npos) { + LOG_DEBUG("using x0 prediction"); + flux_params.chroma_radiance_params.use_x0 = true; + } + if (tensor_name.find("__32x32__") != std::string::npos) { + LOG_DEBUG("using patch size 32 prediction"); + flux_params.chroma_radiance_params.use_patch_size_32 = true; + flux_params.patch_size = 32; + } if (tensor_name.find("distilled_guidance_layer.in_proj.weight") != std::string::npos) { // Chroma flux_params.is_chroma = true; diff --git a/model.cpp b/model.cpp index 01a8c45de..561eb3cc0 100644 --- a/model.cpp +++ b/model.cpp @@ -1737,6 +1737,13 @@ bool ModelLoader::save_to_gguf_file(const std::string& file_path, ggml_type type // tensor_storage.ne[0], tensor_storage.ne[1], tensor_storage.ne[2], tensor_storage.ne[3], // tensor->n_dims, tensor->ne[0], tensor->ne[1], tensor->ne[2], tensor->ne[3]); + if (!tensor->data) { + GGML_ASSERT(ggml_nelements(tensor) == 0); + // avoid crashing the gguf writer by setting a dummy pointer for zero-sized tensors + LOG_DEBUG("setting dummy pointer for zero-sized tensor %s", name.c_str()); + tensor->data = ggml_get_mem_buffer(ggml_ctx); + } + *dst_tensor = tensor; gguf_add_tensor(gguf_ctx, tensor); diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 6446c4129..17fe3fdee 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -708,6 +708,8 @@ class StableDiffusionGGML { if (stacked_id) { ignore_tensors.insert("pmid.unet."); } + ignore_tensors.insert("model.diffusion_model.__x0__"); + ignore_tensors.insert("model.diffusion_model.__32x32__"); if (vae_decode_only) { ignore_tensors.insert("first_stage_model.encoder"); @@ -842,6 +844,7 @@ class StableDiffusionGGML { } } else if (sd_version_is_flux(version)) { pred_type = FLUX_FLOW_PRED; + if (flow_shift == INFINITY) { flow_shift = 1.0f; // TODO: validate for (const auto& [name, tensor_storage] : tensor_storage_map) { From 60abda56e07455f1d008b221de0f4ef4c4136741 Mon Sep 17 00:00:00 2001 From: stduhpf Date: Sun, 21 Dec 2025 08:35:38 +0100 Subject: [PATCH 31/49] feat: select vulkan device with env variable (#629) --- stable-diffusion.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 17fe3fdee..74519938d 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -165,7 +165,27 @@ class StableDiffusionGGML { #endif #ifdef SD_USE_VULKAN LOG_DEBUG("Using Vulkan backend"); - for (int device = 0; device < ggml_backend_vk_get_device_count(); ++device) { + size_t device = 0; + const int device_count = ggml_backend_vk_get_device_count(); + if (device_count) { + const char* SD_VK_DEVICE = getenv("SD_VK_DEVICE"); + if (SD_VK_DEVICE != nullptr) { + std::string sd_vk_device_str = SD_VK_DEVICE; + try { + device = std::stoull(sd_vk_device_str); + } catch (const std::invalid_argument&) { + LOG_WARN("SD_VK_DEVICE environment variable is not a valid integer (%s). Falling back to device 0.", SD_VK_DEVICE); + device = 0; + } catch (const std::out_of_range&) { + LOG_WARN("SD_VK_DEVICE environment variable value is out of range for `unsigned long long` type (%s). Falling back to device 0.", SD_VK_DEVICE); + device = 0; + } + if (device >= device_count) { + LOG_WARN("Cannot find targeted vulkan device (%llu). Falling back to device 0.", device); + device = 0; + } + } + LOG_INFO("Vulkan: Using device %llu", device); backend = ggml_backend_vk_init(device); } if (!backend) { From 88ec9d30b1a149cf5bdb14bf9df11d966f91bf80 Mon Sep 17 00:00:00 2001 From: leejet Date: Sun, 21 Dec 2025 15:40:21 +0800 Subject: [PATCH 32/49] feat: add scale_rope support (#1121) --- rope.hpp | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/rope.hpp b/rope.hpp index 4abc51469..12047e3e9 100644 --- a/rope.hpp +++ b/rope.hpp @@ -91,14 +91,23 @@ namespace Rope { int axes_dim_num, int index = 0, int h_offset = 0, - int w_offset = 0) { + int w_offset = 0, + bool scale_rope = false) { int h_len = (h + (patch_size / 2)) / patch_size; int w_len = (w + (patch_size / 2)) / patch_size; std::vector> img_ids(h_len * w_len, std::vector(axes_dim_num, 0.0)); - std::vector row_ids = linspace(h_offset, h_len - 1 + h_offset, h_len); - std::vector col_ids = linspace(w_offset, w_len - 1 + w_offset, w_len); + int h_start = h_offset; + int w_start = w_offset; + + if (scale_rope) { + h_start -= h_len / 2; + w_start -= w_len / 2; + } + + std::vector row_ids = linspace(h_start, h_start + h_len - 1, h_len); + std::vector col_ids = linspace(w_start, w_start + w_len - 1, w_len); for (int i = 0; i < h_len; ++i) { for (int j = 0; j < w_len; ++j) { @@ -171,7 +180,8 @@ namespace Rope { int axes_dim_num, const std::vector& ref_latents, bool increase_ref_index, - float ref_index_scale) { + float ref_index_scale, + bool scale_rope) { std::vector> ids; uint64_t curr_h_offset = 0; uint64_t curr_w_offset = 0; @@ -185,6 +195,7 @@ namespace Rope { } else { h_offset = curr_h_offset; } + scale_rope = false; } auto ref_ids = gen_flux_img_ids(ref->ne[1], @@ -194,7 +205,8 @@ namespace Rope { axes_dim_num, static_cast(index * ref_index_scale), h_offset, - w_offset); + w_offset, + scale_rope); ids = concat_ids(ids, ref_ids, bs); if (increase_ref_index) { @@ -222,7 +234,7 @@ namespace Rope { auto ids = concat_ids(txt_ids, img_ids, bs); if (ref_latents.size() > 0) { - auto refs_ids = gen_refs_ids(patch_size, bs, axes_dim_num, ref_latents, increase_ref_index, ref_index_scale); + auto refs_ids = gen_refs_ids(patch_size, bs, axes_dim_num, ref_latents, increase_ref_index, ref_index_scale, false); ids = concat_ids(ids, refs_ids, bs); } return ids; @@ -271,10 +283,10 @@ namespace Rope { } } int axes_dim_num = 3; - auto img_ids = gen_flux_img_ids(h, w, patch_size, bs, axes_dim_num); + auto img_ids = gen_flux_img_ids(h, w, patch_size, bs, axes_dim_num, 0, 0, 0, true); auto ids = concat_ids(txt_ids_repeated, img_ids, bs); if (ref_latents.size() > 0) { - auto refs_ids = gen_refs_ids(patch_size, bs, axes_dim_num, ref_latents, increase_ref_index, 1.f); + auto refs_ids = gen_refs_ids(patch_size, bs, axes_dim_num, ref_latents, increase_ref_index, 1.f, true); ids = concat_ids(ids, refs_ids, bs); } return ids; From 50ff966445ba5b0d435238c2aead7c5f26835b09 Mon Sep 17 00:00:00 2001 From: Phylliida Dev Date: Sun, 21 Dec 2025 03:06:47 -0700 Subject: [PATCH 33/49] feat: add seamless texture generation support (#914) * global bool * reworked circular to global flag * cleaner implementation of tiling support in sd cpp * cleaned rope * working simplified but still need wraps * Further clean of rope * resolve flux conflict * switch to pad op circular only * Set ggml to most recent * Revert ggml temp * Update ggml to most recent * Revert unneded flux change * move circular flag to the GGMLRunnerContext * Pass through circular param in all places where conv is called * fix of constant and minor cleanup * Added back --circular option * Conv2d circular in vae and various models * Fix temporal padding for qwen image and other vaes * Z Image circular tiling * x and y axis seamless only * First attempt at chroma seamless x and y * refactor into pure x and y, almost there * Fix crash on chroma * Refactor into cleaner variable choices * Removed redundant set_circular_enabled * Sync ggml * simplify circular parameter * format code * no need to perform circular pad on the clip * simplify circular_axes setting * unify function naming * remove unnecessary member variables * simplify rope --------- Co-authored-by: Phylliida Co-authored-by: leejet --- common.hpp | 2 +- denoiser.hpp | 6 +- diffusion_model.hpp | 29 ++++++- examples/common/common.hpp | 21 +++++ flux.hpp | 28 ++++--- ggml | 2 +- ggml_extend.hpp | 119 +++++++++++++++++++++------ lora.hpp | 8 ++ mmdit.hpp | 2 +- qwen_image.hpp | 16 ++-- rope.hpp | 164 ++++++++++++++++++++++++++++++++++--- stable-diffusion.cpp | 26 +++++- stable-diffusion.h | 2 + wan.hpp | 15 ++-- z_image.hpp | 14 ++-- 15 files changed, 375 insertions(+), 79 deletions(-) diff --git a/common.hpp b/common.hpp index 74b218ab7..b17c11e35 100644 --- a/common.hpp +++ b/common.hpp @@ -28,7 +28,7 @@ class DownSampleBlock : public GGMLBlock { if (vae_downsample) { auto conv = std::dynamic_pointer_cast(blocks["conv"]); - x = ggml_pad(ctx->ggml_ctx, x, 1, 1, 0, 0); + x = ggml_ext_pad(ctx->ggml_ctx, x, 1, 1, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); x = conv->forward(ctx, x); } else { auto conv = std::dynamic_pointer_cast(blocks["op"]); diff --git a/denoiser.hpp b/denoiser.hpp index fc5230d7b..7a8242e7d 100644 --- a/denoiser.hpp +++ b/denoiser.hpp @@ -366,18 +366,18 @@ struct KLOptimalScheduler : SigmaScheduler { for (uint32_t i = 0; i < n; ++i) { // t goes from 0.0 to 1.0 - float t = static_cast(i) / static_cast(n-1); + float t = static_cast(i) / static_cast(n - 1); // Interpolate in the angle domain float angle = t * alpha_min + (1.0f - t) * alpha_max; // Convert back to sigma sigmas.push_back(std::tan(angle)); - } + } // Append the final zero to sigma sigmas.push_back(0.0f); - + return sigmas; } }; diff --git a/diffusion_model.hpp b/diffusion_model.hpp index 8c741fdc4..c4e0ba1d0 100644 --- a/diffusion_model.hpp +++ b/diffusion_model.hpp @@ -37,8 +37,9 @@ struct DiffusionModel { virtual void get_param_tensors(std::map& tensors) = 0; virtual size_t get_params_buffer_size() = 0; virtual void set_weight_adapter(const std::shared_ptr& adapter){}; - virtual int64_t get_adm_in_channels() = 0; - virtual void set_flash_attn_enabled(bool enabled) = 0; + virtual int64_t get_adm_in_channels() = 0; + virtual void set_flash_attn_enabled(bool enabled) = 0; + virtual void set_circular_axes(bool circular_x, bool circular_y) = 0; }; struct UNetModel : public DiffusionModel { @@ -87,6 +88,10 @@ struct UNetModel : public DiffusionModel { unet.set_flash_attention_enabled(enabled); } + void set_circular_axes(bool circular_x, bool circular_y) override { + unet.set_circular_axes(circular_x, circular_y); + } + bool compute(int n_threads, DiffusionParams diffusion_params, struct ggml_tensor** output = nullptr, @@ -148,6 +153,10 @@ struct MMDiTModel : public DiffusionModel { mmdit.set_flash_attention_enabled(enabled); } + void set_circular_axes(bool circular_x, bool circular_y) override { + mmdit.set_circular_axes(circular_x, circular_y); + } + bool compute(int n_threads, DiffusionParams diffusion_params, struct ggml_tensor** output = nullptr, @@ -210,6 +219,10 @@ struct FluxModel : public DiffusionModel { flux.set_flash_attention_enabled(enabled); } + void set_circular_axes(bool circular_x, bool circular_y) override { + flux.set_circular_axes(circular_x, circular_y); + } + bool compute(int n_threads, DiffusionParams diffusion_params, struct ggml_tensor** output = nullptr, @@ -277,6 +290,10 @@ struct WanModel : public DiffusionModel { wan.set_flash_attention_enabled(enabled); } + void set_circular_axes(bool circular_x, bool circular_y) override { + wan.set_circular_axes(circular_x, circular_y); + } + bool compute(int n_threads, DiffusionParams diffusion_params, struct ggml_tensor** output = nullptr, @@ -343,6 +360,10 @@ struct QwenImageModel : public DiffusionModel { qwen_image.set_flash_attention_enabled(enabled); } + void set_circular_axes(bool circular_x, bool circular_y) override { + qwen_image.set_circular_axes(circular_x, circular_y); + } + bool compute(int n_threads, DiffusionParams diffusion_params, struct ggml_tensor** output = nullptr, @@ -406,6 +427,10 @@ struct ZImageModel : public DiffusionModel { z_image.set_flash_attention_enabled(enabled); } + void set_circular_axes(bool circular_x, bool circular_y) override { + z_image.set_circular_axes(circular_x, circular_y); + } + bool compute(int n_threads, DiffusionParams diffusion_params, struct ggml_tensor** output = nullptr, diff --git a/examples/common/common.hpp b/examples/common/common.hpp index b81dab784..5168730aa 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -449,6 +449,10 @@ struct SDContextParams { bool diffusion_conv_direct = false; bool vae_conv_direct = false; + bool circular = false; + bool circular_x = false; + bool circular_y = false; + bool chroma_use_dit_mask = true; bool chroma_use_t5_mask = false; int chroma_t5_mask_pad = 1; @@ -605,6 +609,18 @@ struct SDContextParams { "--vae-conv-direct", "use ggml_conv2d_direct in the vae model", true, &vae_conv_direct}, + {"", + "--circular", + "enable circular padding for convolutions", + true, &circular}, + {"", + "--circularx", + "enable circular RoPE wrapping on x-axis (width) only", + true, &circular_x}, + {"", + "--circulary", + "enable circular RoPE wrapping on y-axis (height) only", + true, &circular_y}, {"", "--chroma-disable-dit-mask", "disable dit mask for chroma", @@ -868,6 +884,9 @@ struct SDContextParams { << " diffusion_flash_attn: " << (diffusion_flash_attn ? "true" : "false") << ",\n" << " diffusion_conv_direct: " << (diffusion_conv_direct ? "true" : "false") << ",\n" << " vae_conv_direct: " << (vae_conv_direct ? "true" : "false") << ",\n" + << " circular: " << (circular ? "true" : "false") << ",\n" + << " circular_x: " << (circular_x ? "true" : "false") << ",\n" + << " circular_y: " << (circular_y ? "true" : "false") << ",\n" << " chroma_use_dit_mask: " << (chroma_use_dit_mask ? "true" : "false") << ",\n" << " chroma_use_t5_mask: " << (chroma_use_t5_mask ? "true" : "false") << ",\n" << " chroma_t5_mask_pad: " << chroma_t5_mask_pad << ",\n" @@ -928,6 +947,8 @@ struct SDContextParams { taesd_preview, diffusion_conv_direct, vae_conv_direct, + circular || circular_x, + circular || circular_y, force_sdxl_vae_conv_scale, chroma_use_dit_mask, chroma_use_t5_mask, diff --git a/flux.hpp b/flux.hpp index 7ce263569..bcff04cfa 100644 --- a/flux.hpp +++ b/flux.hpp @@ -860,14 +860,14 @@ namespace Flux { } } - struct ggml_tensor* pad_to_patch_size(struct ggml_context* ctx, + struct ggml_tensor* pad_to_patch_size(GGMLRunnerContext* ctx, struct ggml_tensor* x) { int64_t W = x->ne[0]; int64_t H = x->ne[1]; int pad_h = (params.patch_size - H % params.patch_size) % params.patch_size; int pad_w = (params.patch_size - W % params.patch_size) % params.patch_size; - x = ggml_pad(ctx, x, pad_w, pad_h, 0, 0); // [N, C, H + pad_h, W + pad_w] + x = ggml_ext_pad(ctx->ggml_ctx, x, pad_w, pad_h, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); return x; } @@ -893,11 +893,11 @@ namespace Flux { return x; } - struct ggml_tensor* process_img(struct ggml_context* ctx, + struct ggml_tensor* process_img(GGMLRunnerContext* ctx, struct ggml_tensor* x) { // img = rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch_size, pw=patch_size) x = pad_to_patch_size(ctx, x); - x = patchify(ctx, x); + x = patchify(ctx->ggml_ctx, x); return x; } @@ -1076,7 +1076,7 @@ namespace Flux { int pad_h = (patch_size - H % patch_size) % patch_size; int pad_w = (patch_size - W % patch_size) % patch_size; - auto img = pad_to_patch_size(ctx->ggml_ctx, x); + auto img = pad_to_patch_size(ctx, x); auto orig_img = img; if (params.chroma_radiance_params.use_patch_size_32) { @@ -1150,7 +1150,7 @@ namespace Flux { int pad_h = (patch_size - H % patch_size) % patch_size; int pad_w = (patch_size - W % patch_size) % patch_size; - auto img = process_img(ctx->ggml_ctx, x); + auto img = process_img(ctx, x); uint64_t img_tokens = img->ne[1]; if (params.version == VERSION_FLUX_FILL) { @@ -1158,8 +1158,8 @@ namespace Flux { ggml_tensor* masked = ggml_view_4d(ctx->ggml_ctx, c_concat, c_concat->ne[0], c_concat->ne[1], C, 1, c_concat->nb[1], c_concat->nb[2], c_concat->nb[3], 0); ggml_tensor* mask = ggml_view_4d(ctx->ggml_ctx, c_concat, c_concat->ne[0], c_concat->ne[1], 8 * 8, 1, c_concat->nb[1], c_concat->nb[2], c_concat->nb[3], c_concat->nb[2] * C); - masked = process_img(ctx->ggml_ctx, masked); - mask = process_img(ctx->ggml_ctx, mask); + masked = process_img(ctx, masked); + mask = process_img(ctx, mask); img = ggml_concat(ctx->ggml_ctx, img, ggml_concat(ctx->ggml_ctx, masked, mask, 0), 0); } else if (params.version == VERSION_FLEX_2) { @@ -1168,21 +1168,21 @@ namespace Flux { ggml_tensor* mask = ggml_view_4d(ctx->ggml_ctx, c_concat, c_concat->ne[0], c_concat->ne[1], 1, 1, c_concat->nb[1], c_concat->nb[2], c_concat->nb[3], c_concat->nb[2] * C); ggml_tensor* control = ggml_view_4d(ctx->ggml_ctx, c_concat, c_concat->ne[0], c_concat->ne[1], C, 1, c_concat->nb[1], c_concat->nb[2], c_concat->nb[3], c_concat->nb[2] * (C + 1)); - masked = process_img(ctx->ggml_ctx, masked); - mask = process_img(ctx->ggml_ctx, mask); - control = process_img(ctx->ggml_ctx, control); + masked = process_img(ctx, masked); + mask = process_img(ctx, mask); + control = process_img(ctx, control); img = ggml_concat(ctx->ggml_ctx, img, ggml_concat(ctx->ggml_ctx, ggml_concat(ctx->ggml_ctx, masked, mask, 0), control, 0), 0); } else if (params.version == VERSION_FLUX_CONTROLS) { GGML_ASSERT(c_concat != nullptr); - auto control = process_img(ctx->ggml_ctx, c_concat); + auto control = process_img(ctx, c_concat); img = ggml_concat(ctx->ggml_ctx, img, control, 0); } if (ref_latents.size() > 0) { for (ggml_tensor* ref : ref_latents) { - ref = process_img(ctx->ggml_ctx, ref); + ref = process_img(ctx, ref); img = ggml_concat(ctx->ggml_ctx, img, ref, 1); } } @@ -1472,6 +1472,8 @@ namespace Flux { increase_ref_index, flux_params.ref_index_scale, flux_params.theta, + circular_y_enabled, + circular_x_enabled, flux_params.axes_dim); int pos_len = pe_vec.size() / flux_params.axes_dim_sum / 2; // LOG_DEBUG("pos_len %d", pos_len); diff --git a/ggml b/ggml index f5425c0ee..3e9f2ba3b 160000 --- a/ggml +++ b/ggml @@ -1 +1 @@ -Subproject commit f5425c0ee5e582a7d64411f06139870bff3e52e0 +Subproject commit 3e9f2ba3b934c20b26873b3c60dbf41b116978ff diff --git a/ggml_extend.hpp b/ggml_extend.hpp index fcaa92c9e..3849562f0 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -993,6 +994,48 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_linear(struct ggml_context* ctx, return x; } +__STATIC_INLINE__ struct ggml_tensor* ggml_ext_pad_ext(struct ggml_context* ctx, + struct ggml_tensor* x, + int lp0, + int rp0, + int lp1, + int rp1, + int lp2, + int rp2, + int lp3, + int rp3, + bool circular_x = false, + bool circular_y = false) { + if (circular_x && circular_y) { + return ggml_pad_ext_circular(ctx, x, lp0, rp0, lp1, rp1, lp2, rp2, lp3, rp3); + } + + if (circular_x && (lp0 != 0 || rp0 != 0)) { + x = ggml_pad_ext_circular(ctx, x, lp0, rp0, 0, 0, 0, 0, 0, 0); + lp0 = rp0 = 0; + } + if (circular_y && (lp1 != 0 || rp1 != 0)) { + x = ggml_pad_ext_circular(ctx, x, 0, 0, lp1, rp1, 0, 0, 0, 0); + lp1 = rp1 = 0; + } + + if (lp0 != 0 || rp0 != 0 || lp1 != 0 || rp1 != 0 || lp2 != 0 || rp2 != 0 || lp3 != 0 || rp3 != 0) { + x = ggml_pad_ext(ctx, x, lp0, rp0, lp1, rp1, lp2, rp2, lp3, rp3); + } + return x; +} + +__STATIC_INLINE__ struct ggml_tensor* ggml_ext_pad(struct ggml_context* ctx, + struct ggml_tensor* x, + int p0, + int p1, + int p2 = 0, + int p3 = 0, + bool circular_x = false, + bool circular_y = false) { + return ggml_ext_pad_ext(ctx, x, p0, p0, p1, p1, p2, p2, p3, p3, circular_x, circular_y); +} + // w: [OC,IC, KH, KW] // x: [N, IC, IH, IW] // b: [OC,] @@ -1001,20 +1044,29 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_conv_2d(struct ggml_context* ctx, struct ggml_tensor* x, struct ggml_tensor* w, struct ggml_tensor* b, - int s0 = 1, - int s1 = 1, - int p0 = 0, - int p1 = 0, - int d0 = 1, - int d1 = 1, - bool direct = false, - float scale = 1.f) { + int s0 = 1, + int s1 = 1, + int p0 = 0, + int p1 = 0, + int d0 = 1, + int d1 = 1, + bool direct = false, + bool circular_x = false, + bool circular_y = false, + float scale = 1.f) { if (scale != 1.f) { x = ggml_scale(ctx, x, scale); } if (w->ne[2] != x->ne[2] && ggml_n_dims(w) == 2) { w = ggml_reshape_4d(ctx, w, 1, 1, w->ne[0], w->ne[1]); } + + if ((p0 != 0 || p1 != 0) && (circular_x || circular_y)) { + x = ggml_ext_pad(ctx, x, p0, p1, 0, 0, circular_x, circular_y); + p0 = 0; + p1 = 0; + } + if (direct) { x = ggml_conv_2d_direct(ctx, w, x, s0, s1, p0, p1, d0, d1); } else { @@ -1521,14 +1573,16 @@ struct WeightAdapter { float scale = 1.f; } linear; struct { - int s0 = 1; - int s1 = 1; - int p0 = 0; - int p1 = 0; - int d0 = 1; - int d1 = 1; - bool direct = false; - float scale = 1.f; + int s0 = 1; + int s1 = 1; + int p0 = 0; + int p1 = 0; + int d0 = 1; + int d1 = 1; + bool direct = false; + bool circular_x = false; + bool circular_y = false; + float scale = 1.f; } conv2d; }; virtual ggml_tensor* patch_weight(ggml_context* ctx, ggml_tensor* weight, const std::string& weight_name) = 0; @@ -1546,6 +1600,8 @@ struct GGMLRunnerContext { ggml_context* ggml_ctx = nullptr; bool flash_attn_enabled = false; bool conv2d_direct_enabled = false; + bool circular_x_enabled = false; + bool circular_y_enabled = false; std::shared_ptr weight_adapter = nullptr; }; @@ -1582,6 +1638,8 @@ struct GGMLRunner { bool flash_attn_enabled = false; bool conv2d_direct_enabled = false; + bool circular_x_enabled = false; + bool circular_y_enabled = false; void alloc_params_ctx() { struct ggml_init_params params; @@ -1859,6 +1917,8 @@ struct GGMLRunner { runner_ctx.backend = runtime_backend; runner_ctx.flash_attn_enabled = flash_attn_enabled; runner_ctx.conv2d_direct_enabled = conv2d_direct_enabled; + runner_ctx.circular_x_enabled = circular_x_enabled; + runner_ctx.circular_y_enabled = circular_y_enabled; runner_ctx.weight_adapter = weight_adapter; return runner_ctx; } @@ -2003,6 +2063,11 @@ struct GGMLRunner { conv2d_direct_enabled = enabled; } + void set_circular_axes(bool circular_x, bool circular_y) { + circular_x_enabled = circular_x; + circular_y_enabled = circular_y; + } + void set_weight_adapter(const std::shared_ptr& adapter) { weight_adapter = adapter; } @@ -2266,15 +2331,17 @@ class Conv2d : public UnaryBlock { } if (ctx->weight_adapter) { WeightAdapter::ForwardParams forward_params; - forward_params.op_type = WeightAdapter::ForwardParams::op_type_t::OP_CONV2D; - forward_params.conv2d.s0 = stride.second; - forward_params.conv2d.s1 = stride.first; - forward_params.conv2d.p0 = padding.second; - forward_params.conv2d.p1 = padding.first; - forward_params.conv2d.d0 = dilation.second; - forward_params.conv2d.d1 = dilation.first; - forward_params.conv2d.direct = ctx->conv2d_direct_enabled; - forward_params.conv2d.scale = scale; + forward_params.op_type = WeightAdapter::ForwardParams::op_type_t::OP_CONV2D; + forward_params.conv2d.s0 = stride.second; + forward_params.conv2d.s1 = stride.first; + forward_params.conv2d.p0 = padding.second; + forward_params.conv2d.p1 = padding.first; + forward_params.conv2d.d0 = dilation.second; + forward_params.conv2d.d1 = dilation.first; + forward_params.conv2d.direct = ctx->conv2d_direct_enabled; + forward_params.conv2d.circular_x = ctx->circular_x_enabled; + forward_params.conv2d.circular_y = ctx->circular_y_enabled; + forward_params.conv2d.scale = scale; return ctx->weight_adapter->forward_with_lora(ctx->ggml_ctx, x, w, b, prefix, forward_params); } return ggml_ext_conv_2d(ctx->ggml_ctx, @@ -2288,6 +2355,8 @@ class Conv2d : public UnaryBlock { dilation.second, dilation.first, ctx->conv2d_direct_enabled, + ctx->circular_x_enabled, + ctx->circular_y_enabled, scale); } }; diff --git a/lora.hpp b/lora.hpp index b847f044c..7d83ec5cd 100644 --- a/lora.hpp +++ b/lora.hpp @@ -599,6 +599,8 @@ struct LoraModel : public GGMLRunner { forward_params.conv2d.d0, forward_params.conv2d.d1, forward_params.conv2d.direct, + forward_params.conv2d.circular_x, + forward_params.conv2d.circular_y, forward_params.conv2d.scale); if (lora_mid) { lx = ggml_ext_conv_2d(ctx, @@ -612,6 +614,8 @@ struct LoraModel : public GGMLRunner { 1, 1, forward_params.conv2d.direct, + forward_params.conv2d.circular_x, + forward_params.conv2d.circular_y, forward_params.conv2d.scale); } lx = ggml_ext_conv_2d(ctx, @@ -625,6 +629,8 @@ struct LoraModel : public GGMLRunner { 1, 1, forward_params.conv2d.direct, + forward_params.conv2d.circular_x, + forward_params.conv2d.circular_y, forward_params.conv2d.scale); } @@ -779,6 +785,8 @@ struct MultiLoraAdapter : public WeightAdapter { forward_params.conv2d.d0, forward_params.conv2d.d1, forward_params.conv2d.direct, + forward_params.conv2d.circular_x, + forward_params.conv2d.circular_y, forward_params.conv2d.scale); } for (auto& lora_model : lora_models) { diff --git a/mmdit.hpp b/mmdit.hpp index 38bdc2e74..eeb74a268 100644 --- a/mmdit.hpp +++ b/mmdit.hpp @@ -983,4 +983,4 @@ struct MMDiTRunner : public GGMLRunner { } }; -#endif \ No newline at end of file +#endif diff --git a/qwen_image.hpp b/qwen_image.hpp index eeb823d50..ed1c98308 100644 --- a/qwen_image.hpp +++ b/qwen_image.hpp @@ -354,14 +354,14 @@ namespace Qwen { blocks["proj_out"] = std::shared_ptr(new Linear(inner_dim, params.patch_size * params.patch_size * params.out_channels)); } - struct ggml_tensor* pad_to_patch_size(struct ggml_context* ctx, + struct ggml_tensor* pad_to_patch_size(GGMLRunnerContext* ctx, struct ggml_tensor* x) { int64_t W = x->ne[0]; int64_t H = x->ne[1]; int pad_h = (params.patch_size - H % params.patch_size) % params.patch_size; int pad_w = (params.patch_size - W % params.patch_size) % params.patch_size; - x = ggml_pad(ctx, x, pad_w, pad_h, 0, 0); // [N, C, H + pad_h, W + pad_w] + x = ggml_ext_pad(ctx->ggml_ctx, x, pad_w, pad_h, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); return x; } @@ -387,10 +387,10 @@ namespace Qwen { return x; } - struct ggml_tensor* process_img(struct ggml_context* ctx, + struct ggml_tensor* process_img(GGMLRunnerContext* ctx, struct ggml_tensor* x) { x = pad_to_patch_size(ctx, x); - x = patchify(ctx, x); + x = patchify(ctx->ggml_ctx, x); return x; } @@ -466,12 +466,12 @@ namespace Qwen { int64_t C = x->ne[2]; int64_t N = x->ne[3]; - auto img = process_img(ctx->ggml_ctx, x); + auto img = process_img(ctx, x); uint64_t img_tokens = img->ne[1]; if (ref_latents.size() > 0) { for (ggml_tensor* ref : ref_latents) { - ref = process_img(ctx->ggml_ctx, ref); + ref = process_img(ctx, ref); img = ggml_concat(ctx->ggml_ctx, img, ref, 1); } } @@ -565,6 +565,8 @@ namespace Qwen { ref_latents, increase_ref_index, qwen_image_params.theta, + circular_y_enabled, + circular_x_enabled, qwen_image_params.axes_dim); int pos_len = pe_vec.size() / qwen_image_params.axes_dim_sum / 2; // LOG_DEBUG("pos_len %d", pos_len); @@ -684,4 +686,4 @@ namespace Qwen { } // namespace name -#endif // __QWEN_IMAGE_HPP__ \ No newline at end of file +#endif // __QWEN_IMAGE_HPP__ diff --git a/rope.hpp b/rope.hpp index 12047e3e9..4e6136c11 100644 --- a/rope.hpp +++ b/rope.hpp @@ -1,6 +1,8 @@ #ifndef __ROPE_HPP__ #define __ROPE_HPP__ +#include +#include #include #include "ggml_extend.hpp" @@ -39,7 +41,10 @@ namespace Rope { return flat_vec; } - __STATIC_INLINE__ std::vector> rope(const std::vector& pos, int dim, int theta) { + __STATIC_INLINE__ std::vector> rope(const std::vector& pos, + int dim, + int theta, + const std::vector& axis_wrap_dims = {}) { assert(dim % 2 == 0); int half_dim = dim / 2; @@ -47,14 +52,31 @@ namespace Rope { std::vector omega(half_dim); for (int i = 0; i < half_dim; ++i) { - omega[i] = 1.0 / std::pow(theta, scale[i]); + omega[i] = 1.0f / std::pow(theta, scale[i]); } int pos_size = pos.size(); std::vector> out(pos_size, std::vector(half_dim)); for (int i = 0; i < pos_size; ++i) { for (int j = 0; j < half_dim; ++j) { - out[i][j] = pos[i] * omega[j]; + float angle = pos[i] * omega[j]; + if (!axis_wrap_dims.empty()) { + size_t wrap_size = axis_wrap_dims.size(); + // mod batch size since we only store this for one item in the batch + size_t wrap_idx = wrap_size > 0 ? (i % wrap_size) : 0; + int wrap_dim = axis_wrap_dims[wrap_idx]; + if (wrap_dim > 0) { + constexpr float TWO_PI = 6.28318530717958647692f; + float cycles = omega[j] * wrap_dim / TWO_PI; + // closest periodic harmonic, necessary to ensure things neatly tile + // without this round, things don't tile at the boundaries and you end up + // with the model knowing what is "center" + float rounded = std::round(cycles); + angle = pos[i] * TWO_PI * rounded / wrap_dim; + } + } + + out[i][j] = angle; } } @@ -89,9 +111,9 @@ namespace Rope { int patch_size, int bs, int axes_dim_num, - int index = 0, - int h_offset = 0, - int w_offset = 0, + int index = 0, + int h_offset = 0, + int w_offset = 0, bool scale_rope = false) { int h_len = (h + (patch_size / 2)) / patch_size; int w_len = (w + (patch_size / 2)) / patch_size; @@ -146,7 +168,8 @@ namespace Rope { __STATIC_INLINE__ std::vector embed_nd(const std::vector>& ids, int bs, int theta, - const std::vector& axes_dim) { + const std::vector& axes_dim, + const std::vector>& wrap_dims = {}) { std::vector> trans_ids = transpose(ids); size_t pos_len = ids.size() / bs; int num_axes = axes_dim.size(); @@ -161,7 +184,12 @@ namespace Rope { std::vector> emb(bs * pos_len, std::vector(emb_dim * 2 * 2, 0.0)); int offset = 0; for (int i = 0; i < num_axes; ++i) { - std::vector> rope_emb = rope(trans_ids[i], axes_dim[i], theta); // [bs*pos_len, axes_dim[i]/2 * 2 * 2] + std::vector axis_wrap_dims; + if (!wrap_dims.empty() && i < (int)wrap_dims.size()) { + axis_wrap_dims = wrap_dims[i]; + } + std::vector> rope_emb = + rope(trans_ids[i], axes_dim[i], theta, axis_wrap_dims); // [bs*pos_len, axes_dim[i]/2 * 2 * 2] for (int b = 0; b < bs; ++b) { for (int j = 0; j < pos_len; ++j) { for (int k = 0; k < rope_emb[0].size(); ++k) { @@ -251,6 +279,8 @@ namespace Rope { bool increase_ref_index, float ref_index_scale, int theta, + bool circular_h, + bool circular_w, const std::vector& axes_dim) { std::vector> ids = gen_flux_ids(h, w, @@ -262,7 +292,47 @@ namespace Rope { ref_latents, increase_ref_index, ref_index_scale); - return embed_nd(ids, bs, theta, axes_dim); + std::vector> wrap_dims; + if ((circular_h || circular_w) && bs > 0 && axes_dim.size() >= 3) { + int h_len = (h + (patch_size / 2)) / patch_size; + int w_len = (w + (patch_size / 2)) / patch_size; + if (h_len > 0 && w_len > 0) { + size_t pos_len = ids.size() / bs; + wrap_dims.assign(axes_dim.size(), std::vector(pos_len, 0)); + size_t cursor = context_len; // text first + const size_t img_tokens = static_cast(h_len) * static_cast(w_len); + for (size_t token_i = 0; token_i < img_tokens; ++token_i) { + if (circular_h) { + wrap_dims[1][cursor + token_i] = h_len; + } + if (circular_w) { + wrap_dims[2][cursor + token_i] = w_len; + } + } + cursor += img_tokens; + // reference latents + for (ggml_tensor* ref : ref_latents) { + if (ref == nullptr) { + continue; + } + int ref_h = static_cast(ref->ne[1]); + int ref_w = static_cast(ref->ne[0]); + int ref_h_l = (ref_h + (patch_size / 2)) / patch_size; + int ref_w_l = (ref_w + (patch_size / 2)) / patch_size; + size_t ref_tokens = static_cast(ref_h_l) * static_cast(ref_w_l); + for (size_t token_i = 0; token_i < ref_tokens; ++token_i) { + if (circular_h) { + wrap_dims[1][cursor + token_i] = ref_h_l; + } + if (circular_w) { + wrap_dims[2][cursor + token_i] = ref_w_l; + } + } + cursor += ref_tokens; + } + } + } + return embed_nd(ids, bs, theta, axes_dim, wrap_dims); } __STATIC_INLINE__ std::vector> gen_qwen_image_ids(int h, @@ -301,9 +371,57 @@ namespace Rope { const std::vector& ref_latents, bool increase_ref_index, int theta, + bool circular_h, + bool circular_w, const std::vector& axes_dim) { std::vector> ids = gen_qwen_image_ids(h, w, patch_size, bs, context_len, ref_latents, increase_ref_index); - return embed_nd(ids, bs, theta, axes_dim); + std::vector> wrap_dims; + // This logic simply stores the (pad and patch_adjusted) sizes of images so we can make sure rope correctly tiles + if ((circular_h || circular_w) && bs > 0 && axes_dim.size() >= 3) { + int pad_h = (patch_size - (h % patch_size)) % patch_size; + int pad_w = (patch_size - (w % patch_size)) % patch_size; + int h_len = (h + pad_h) / patch_size; + int w_len = (w + pad_w) / patch_size; + if (h_len > 0 && w_len > 0) { + const size_t total_tokens = ids.size(); + // Track per-token wrap lengths for the row/column axes so only spatial tokens become periodic. + wrap_dims.assign(axes_dim.size(), std::vector(total_tokens / bs, 0)); + size_t cursor = context_len; // ignore text tokens + const size_t img_tokens = static_cast(h_len) * static_cast(w_len); + for (size_t token_i = 0; token_i < img_tokens; ++token_i) { + if (circular_h) { + wrap_dims[1][cursor + token_i] = h_len; + } + if (circular_w) { + wrap_dims[2][cursor + token_i] = w_len; + } + } + cursor += img_tokens; + // For each reference image, store wrap sizes as well + for (ggml_tensor* ref : ref_latents) { + if (ref == nullptr) { + continue; + } + int ref_h = static_cast(ref->ne[1]); + int ref_w = static_cast(ref->ne[0]); + int ref_pad_h = (patch_size - (ref_h % patch_size)) % patch_size; + int ref_pad_w = (patch_size - (ref_w % patch_size)) % patch_size; + int ref_h_len = (ref_h + ref_pad_h) / patch_size; + int ref_w_len = (ref_w + ref_pad_w) / patch_size; + size_t ref_n_tokens = static_cast(ref_h_len) * static_cast(ref_w_len); + for (size_t token_i = 0; token_i < ref_n_tokens; ++token_i) { + if (circular_h) { + wrap_dims[1][cursor + token_i] = ref_h_len; + } + if (circular_w) { + wrap_dims[2][cursor + token_i] = ref_w_len; + } + } + cursor += ref_n_tokens; + } + } + } + return embed_nd(ids, bs, theta, axes_dim, wrap_dims); } __STATIC_INLINE__ std::vector> gen_vid_ids(int t, @@ -440,9 +558,33 @@ namespace Rope { const std::vector& ref_latents, bool increase_ref_index, int theta, + bool circular_h, + bool circular_w, const std::vector& axes_dim) { std::vector> ids = gen_z_image_ids(h, w, patch_size, bs, context_len, seq_multi_of, ref_latents, increase_ref_index); - return embed_nd(ids, bs, theta, axes_dim); + std::vector> wrap_dims; + if ((circular_h || circular_w) && bs > 0 && axes_dim.size() >= 3) { + int pad_h = (patch_size - (h % patch_size)) % patch_size; + int pad_w = (patch_size - (w % patch_size)) % patch_size; + int h_len = (h + pad_h) / patch_size; + int w_len = (w + pad_w) / patch_size; + if (h_len > 0 && w_len > 0) { + size_t pos_len = ids.size() / bs; + wrap_dims.assign(axes_dim.size(), std::vector(pos_len, 0)); + size_t cursor = context_len + bound_mod(context_len, seq_multi_of); // skip text (and its padding) + size_t img_tokens = static_cast(h_len) * static_cast(w_len); + for (size_t token_i = 0; token_i < img_tokens; ++token_i) { + if (circular_h) { + wrap_dims[1][cursor + token_i] = h_len; + } + if (circular_w) { + wrap_dims[2][cursor + token_i] = w_len; + } + } + } + } + + return embed_nd(ids, bs, theta, axes_dim, wrap_dims); } __STATIC_INLINE__ struct ggml_tensor* apply_rope(struct ggml_context* ctx, diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 74519938d..24516a9bb 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -405,6 +405,10 @@ class StableDiffusionGGML { vae_decode_only = false; } + if (sd_ctx_params->circular_x || sd_ctx_params->circular_y) { + LOG_INFO("Using circular padding for convolutions"); + } + bool clip_on_cpu = sd_ctx_params->keep_clip_on_cpu; { @@ -705,6 +709,20 @@ class StableDiffusionGGML { } pmid_model->get_param_tensors(tensors, "pmid"); } + + diffusion_model->set_circular_axes(sd_ctx_params->circular_x, sd_ctx_params->circular_y); + if (high_noise_diffusion_model) { + high_noise_diffusion_model->set_circular_axes(sd_ctx_params->circular_x, sd_ctx_params->circular_y); + } + if (control_net) { + control_net->set_circular_axes(sd_ctx_params->circular_x, sd_ctx_params->circular_y); + } + if (first_stage_model) { + first_stage_model->set_circular_axes(sd_ctx_params->circular_x, sd_ctx_params->circular_y); + } + if (tae_first_stage) { + tae_first_stage->set_circular_axes(sd_ctx_params->circular_x, sd_ctx_params->circular_y); + } } struct ggml_init_params params; @@ -1519,7 +1537,7 @@ class StableDiffusionGGML { } std::vector skip_layers(guidance.slg.layers, guidance.slg.layers + guidance.slg.layer_count); - float cfg_scale = guidance.txt_cfg; + float cfg_scale = guidance.txt_cfg; if (cfg_scale < 1.f) { if (cfg_scale == 0.f) { // Diffusers follow the convention from the original paper @@ -2559,6 +2577,8 @@ void sd_ctx_params_init(sd_ctx_params_t* sd_ctx_params) { sd_ctx_params->keep_control_net_on_cpu = false; sd_ctx_params->keep_vae_on_cpu = false; sd_ctx_params->diffusion_flash_attn = false; + sd_ctx_params->circular_x = false; + sd_ctx_params->circular_y = false; sd_ctx_params->chroma_use_dit_mask = true; sd_ctx_params->chroma_use_t5_mask = false; sd_ctx_params->chroma_t5_mask_pad = 1; @@ -2598,6 +2618,8 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) { "keep_control_net_on_cpu: %s\n" "keep_vae_on_cpu: %s\n" "diffusion_flash_attn: %s\n" + "circular_x: %s\n" + "circular_y: %s\n" "chroma_use_dit_mask: %s\n" "chroma_use_t5_mask: %s\n" "chroma_t5_mask_pad: %d\n", @@ -2627,6 +2649,8 @@ char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params) { BOOL_STR(sd_ctx_params->keep_control_net_on_cpu), BOOL_STR(sd_ctx_params->keep_vae_on_cpu), BOOL_STR(sd_ctx_params->diffusion_flash_attn), + BOOL_STR(sd_ctx_params->circular_x), + BOOL_STR(sd_ctx_params->circular_y), BOOL_STR(sd_ctx_params->chroma_use_dit_mask), BOOL_STR(sd_ctx_params->chroma_use_t5_mask), sd_ctx_params->chroma_t5_mask_pad); diff --git a/stable-diffusion.h b/stable-diffusion.h index adb65a1d2..30583ea13 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -189,6 +189,8 @@ typedef struct { bool tae_preview_only; bool diffusion_conv_direct; bool vae_conv_direct; + bool circular_x; + bool circular_y; bool force_sdxl_vae_conv_scale; bool chroma_use_dit_mask; bool chroma_use_t5_mask; diff --git a/wan.hpp b/wan.hpp index 75333bfe1..31ecf33f7 100644 --- a/wan.hpp +++ b/wan.hpp @@ -75,7 +75,7 @@ namespace WAN { lp2 -= (int)cache_x->ne[2]; } - x = ggml_pad_ext(ctx->ggml_ctx, x, lp0, rp0, lp1, rp1, lp2, rp2, 0, 0); + x = ggml_ext_pad_ext(ctx->ggml_ctx, x, lp0, rp0, lp1, rp1, lp2, rp2, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); return ggml_ext_conv_3d(ctx->ggml_ctx, x, w, b, in_channels, std::get<2>(stride), std::get<1>(stride), std::get<0>(stride), 0, 0, 0, @@ -206,9 +206,9 @@ namespace WAN { } else if (mode == "upsample3d") { x = ggml_upscale(ctx->ggml_ctx, x, 2, GGML_SCALE_MODE_NEAREST); } else if (mode == "downsample2d") { - x = ggml_pad(ctx->ggml_ctx, x, 1, 1, 0, 0); + x = ggml_ext_pad(ctx->ggml_ctx, x, 1, 1, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); } else if (mode == "downsample3d") { - x = ggml_pad(ctx->ggml_ctx, x, 1, 1, 0, 0); + x = ggml_ext_pad(ctx->ggml_ctx, x, 1, 1, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); } x = resample_1->forward(ctx, x); x = ggml_ext_cont(ctx->ggml_ctx, ggml_ext_torch_permute(ctx->ggml_ctx, x, 0, 1, 3, 2)); // (c, t, h, w) @@ -1826,7 +1826,7 @@ namespace WAN { } } - struct ggml_tensor* pad_to_patch_size(struct ggml_context* ctx, + struct ggml_tensor* pad_to_patch_size(GGMLRunnerContext* ctx, struct ggml_tensor* x) { int64_t W = x->ne[0]; int64_t H = x->ne[1]; @@ -1835,8 +1835,7 @@ namespace WAN { int pad_t = (std::get<0>(params.patch_size) - T % std::get<0>(params.patch_size)) % std::get<0>(params.patch_size); int pad_h = (std::get<1>(params.patch_size) - H % std::get<1>(params.patch_size)) % std::get<1>(params.patch_size); int pad_w = (std::get<2>(params.patch_size) - W % std::get<2>(params.patch_size)) % std::get<2>(params.patch_size); - x = ggml_pad(ctx, x, pad_w, pad_h, pad_t, 0); // [N*C, T + pad_t, H + pad_h, W + pad_w] - + ggml_ext_pad(ctx->ggml_ctx, x, pad_w, pad_h, pad_t, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); return x; } @@ -1986,14 +1985,14 @@ namespace WAN { int64_t T = x->ne[2]; int64_t C = x->ne[3]; - x = pad_to_patch_size(ctx->ggml_ctx, x); + x = pad_to_patch_size(ctx, x); int64_t t_len = ((T + (std::get<0>(params.patch_size) / 2)) / std::get<0>(params.patch_size)); int64_t h_len = ((H + (std::get<1>(params.patch_size) / 2)) / std::get<1>(params.patch_size)); int64_t w_len = ((W + (std::get<2>(params.patch_size) / 2)) / std::get<2>(params.patch_size)); if (time_dim_concat != nullptr) { - time_dim_concat = pad_to_patch_size(ctx->ggml_ctx, time_dim_concat); + time_dim_concat = pad_to_patch_size(ctx, time_dim_concat); x = ggml_concat(ctx->ggml_ctx, x, time_dim_concat, 2); // [N*C, (T+pad_t) + (T2+pad_t2), H + pad_h, W + pad_w] t_len = ((x->ne[2] + (std::get<0>(params.patch_size) / 2)) / std::get<0>(params.patch_size)); } diff --git a/z_image.hpp b/z_image.hpp index bc554f177..af8d57e04 100644 --- a/z_image.hpp +++ b/z_image.hpp @@ -324,14 +324,14 @@ namespace ZImage { blocks["final_layer"] = std::make_shared(z_image_params.hidden_size, z_image_params.patch_size, z_image_params.out_channels); } - struct ggml_tensor* pad_to_patch_size(struct ggml_context* ctx, + struct ggml_tensor* pad_to_patch_size(GGMLRunnerContext* ctx, struct ggml_tensor* x) { int64_t W = x->ne[0]; int64_t H = x->ne[1]; int pad_h = (z_image_params.patch_size - H % z_image_params.patch_size) % z_image_params.patch_size; int pad_w = (z_image_params.patch_size - W % z_image_params.patch_size) % z_image_params.patch_size; - x = ggml_pad(ctx, x, pad_w, pad_h, 0, 0); // [N, C, H + pad_h, W + pad_w] + x = ggml_ext_pad(ctx->ggml_ctx, x, pad_w, pad_h, 0, 0, ctx->circular_x_enabled, ctx->circular_y_enabled); return x; } @@ -357,10 +357,10 @@ namespace ZImage { return x; } - struct ggml_tensor* process_img(struct ggml_context* ctx, + struct ggml_tensor* process_img(GGMLRunnerContext* ctx, struct ggml_tensor* x) { x = pad_to_patch_size(ctx, x); - x = patchify(ctx, x); + x = patchify(ctx->ggml_ctx, x); return x; } @@ -473,12 +473,12 @@ namespace ZImage { int64_t C = x->ne[2]; int64_t N = x->ne[3]; - auto img = process_img(ctx->ggml_ctx, x); + auto img = process_img(ctx, x); uint64_t n_img_token = img->ne[1]; if (ref_latents.size() > 0) { for (ggml_tensor* ref : ref_latents) { - ref = process_img(ctx->ggml_ctx, ref); + ref = process_img(ctx, ref); img = ggml_concat(ctx->ggml_ctx, img, ref, 1); } } @@ -552,6 +552,8 @@ namespace ZImage { ref_latents, increase_ref_index, z_image_params.theta, + circular_y_enabled, + circular_x_enabled, z_image_params.axes_dim); int pos_len = pe_vec.size() / z_image_params.axes_dim_sum / 2; // LOG_DEBUG("pos_len %d", pos_len); From ca5b1969a8f56b21ae841842ced9f06c3145d3b9 Mon Sep 17 00:00:00 2001 From: leejet Date: Sun, 21 Dec 2025 18:40:10 +0800 Subject: [PATCH 34/49] feat: do not convert tensor names by default in convert mode (#1122) --- examples/cli/README.md | 1 + examples/cli/main.cpp | 9 ++++++++- model.cpp | 11 +++++++++-- stable-diffusion.h | 3 ++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/cli/README.md b/examples/cli/README.md index da7734d31..d6cb6aa3b 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -9,6 +9,7 @@ CLI Options: --preview-interval interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at every step) --canny apply canny preprocessor (edge detection) + --convert-name convert tensor name (for convert mode) -v, --verbose print extra info --color colors the logging tags according to level --taesd-preview-only prevents usage of taesd for decoding the final image. (for use with --preview tae) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index a1b92f77e..eb6f9da2f 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -32,6 +32,7 @@ struct SDCliParams { bool verbose = false; bool canny_preprocess = false; + bool convert_name = false; preview_t preview_method = PREVIEW_NONE; int preview_interval = 1; @@ -69,6 +70,10 @@ struct SDCliParams { "--canny", "apply canny preprocessor (edge detection)", true, &canny_preprocess}, + {"", + "--convert-name", + "convert tensor name (for convert mode)", + true, &canny_preprocess}, {"-v", "--verbose", "print extra info", @@ -174,6 +179,7 @@ struct SDCliParams { << " verbose: " << (verbose ? "true" : "false") << ",\n" << " color: " << (color ? "true" : "false") << ",\n" << " canny_preprocess: " << (canny_preprocess ? "true" : "false") << ",\n" + << " convert_name: " << (convert_name ? "true" : "false") << ",\n" << " preview_method: " << previews_str[preview_method] << ",\n" << " preview_interval: " << preview_interval << ",\n" << " preview_path: \"" << preview_path << "\",\n" @@ -387,7 +393,8 @@ int main(int argc, const char* argv[]) { ctx_params.vae_path.c_str(), cli_params.output_path.c_str(), ctx_params.wtype, - ctx_params.tensor_type_rules.c_str()); + ctx_params.tensor_type_rules.c_str(), + cli_params.convert_name); if (!success) { LOG_ERROR("convert '%s'/'%s' to '%s' failed", ctx_params.model_path.c_str(), diff --git a/model.cpp b/model.cpp index 561eb3cc0..b5f734429 100644 --- a/model.cpp +++ b/model.cpp @@ -1783,7 +1783,12 @@ int64_t ModelLoader::get_params_mem_size(ggml_backend_t backend, ggml_type type) return mem_size; } -bool convert(const char* input_path, const char* vae_path, const char* output_path, sd_type_t output_type, const char* tensor_type_rules) { +bool convert(const char* input_path, + const char* vae_path, + const char* output_path, + sd_type_t output_type, + const char* tensor_type_rules, + bool convert_name) { ModelLoader model_loader; if (!model_loader.init_from_file(input_path)) { @@ -1797,7 +1802,9 @@ bool convert(const char* input_path, const char* vae_path, const char* output_pa return false; } } - model_loader.convert_tensors_name(); + if (convert_name) { + model_loader.convert_tensors_name(); + } bool success = model_loader.save_to_gguf_file(output_path, (ggml_type)output_type, tensor_type_rules); return success; } diff --git a/stable-diffusion.h b/stable-diffusion.h index 30583ea13..9bc1fba5b 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -365,7 +365,8 @@ SD_API bool convert(const char* input_path, const char* vae_path, const char* output_path, enum sd_type_t output_type, - const char* tensor_type_rules); + const char* tensor_type_rules, + bool convert_name); SD_API bool preprocess_canny(sd_image_t image, float high_threshold, From c6937ba44ab115abf8f0ee25f391126b792016fe Mon Sep 17 00:00:00 2001 From: leejet Date: Sun, 21 Dec 2025 21:47:50 +0800 Subject: [PATCH 35/49] fix: correct the parsing of --convert-name opotion --- examples/cli/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index eb6f9da2f..c5c4c9a89 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -73,7 +73,7 @@ struct SDCliParams { {"", "--convert-name", "convert tensor name (for convert mode)", - true, &canny_preprocess}, + true, &convert_name, {"-v", "--verbose", "print extra info", From 30a91138f8c94a930452ca403ae5af6981558ea5 Mon Sep 17 00:00:00 2001 From: leejet Date: Sun, 21 Dec 2025 21:53:38 +0800 Subject: [PATCH 36/49] fix: add the missing } --- examples/cli/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index c5c4c9a89..708dbb104 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -73,7 +73,7 @@ struct SDCliParams { {"", "--convert-name", "convert tensor name (for convert mode)", - true, &convert_name, + true, &convert_name}, {"-v", "--verbose", "print extra info", From 298b11069fbf33f8f471ffebc4b6ab3917fdf800 Mon Sep 17 00:00:00 2001 From: rmatif Date: Mon, 22 Dec 2025 16:52:11 +0100 Subject: [PATCH 37/49] feat: add more caching methods (#1066) --- cache_dit.hpp | 975 +++++++++++++++++++++++++++++++++++++ docs/caching.md | 126 +++++ examples/cli/README.md | 9 +- examples/cli/main.cpp | 4 +- examples/common/common.hpp | 271 ++++++++--- examples/server/main.cpp | 4 +- stable-diffusion.cpp | 342 ++++++++++--- stable-diffusion.h | 32 +- ucache.hpp | 404 +++++++++++++++ 9 files changed, 2023 insertions(+), 144 deletions(-) create mode 100644 cache_dit.hpp create mode 100644 docs/caching.md create mode 100644 ucache.hpp diff --git a/cache_dit.hpp b/cache_dit.hpp new file mode 100644 index 000000000..f5a1f180b --- /dev/null +++ b/cache_dit.hpp @@ -0,0 +1,975 @@ +#ifndef __CACHE_DIT_HPP__ +#define __CACHE_DIT_HPP__ + +#include +#include +#include +#include +#include +#include + +#include "ggml_extend.hpp" + +struct DBCacheConfig { + bool enabled = false; + int Fn_compute_blocks = 8; + int Bn_compute_blocks = 0; + float residual_diff_threshold = 0.08f; + int max_warmup_steps = 8; + int max_cached_steps = -1; + int max_continuous_cached_steps = -1; + float max_accumulated_residual_diff = -1.0f; + std::vector steps_computation_mask; + bool scm_policy_dynamic = true; +}; + +struct TaylorSeerConfig { + bool enabled = false; + int n_derivatives = 1; + int max_warmup_steps = 2; + int skip_interval_steps = 1; +}; + +struct CacheDitConfig { + DBCacheConfig dbcache; + TaylorSeerConfig taylorseer; + int double_Fn_blocks = -1; + int double_Bn_blocks = -1; + int single_Fn_blocks = -1; + int single_Bn_blocks = -1; +}; + +struct TaylorSeerState { + int n_derivatives = 1; + int current_step = -1; + int last_computed_step = -1; + std::vector> dY_prev; + std::vector> dY_current; + + void init(int n_deriv, size_t hidden_size) { + n_derivatives = n_deriv; + int order = n_derivatives + 1; + dY_prev.resize(order); + dY_current.resize(order); + for (int i = 0; i < order; i++) { + dY_prev[i].clear(); + dY_current[i].clear(); + } + current_step = -1; + last_computed_step = -1; + } + + void reset() { + for (auto& v : dY_prev) + v.clear(); + for (auto& v : dY_current) + v.clear(); + current_step = -1; + last_computed_step = -1; + } + + bool can_approximate() const { + return last_computed_step >= n_derivatives && !dY_prev.empty() && !dY_prev[0].empty(); + } + + void update_derivatives(const float* Y, size_t size, int step) { + int order = n_derivatives + 1; + dY_prev = dY_current; + dY_current[0].resize(size); + for (size_t i = 0; i < size; i++) { + dY_current[0][i] = Y[i]; + } + + int window = step - last_computed_step; + if (window <= 0) + window = 1; + + for (int d = 0; d < n_derivatives; d++) { + if (!dY_prev[d].empty() && dY_prev[d].size() == size) { + dY_current[d + 1].resize(size); + for (size_t i = 0; i < size; i++) { + dY_current[d + 1][i] = (dY_current[d][i] - dY_prev[d][i]) / static_cast(window); + } + } else { + dY_current[d + 1].clear(); + } + } + + current_step = step; + last_computed_step = step; + } + + void approximate(float* output, size_t size, int target_step) const { + if (!can_approximate() || dY_prev[0].size() != size) { + return; + } + + int elapsed = target_step - last_computed_step; + if (elapsed <= 0) + elapsed = 1; + + std::fill(output, output + size, 0.0f); + float factorial = 1.0f; + int order = static_cast(dY_prev.size()); + + for (int o = 0; o < order; o++) { + if (dY_prev[o].empty() || dY_prev[o].size() != size) + continue; + if (o > 0) + factorial *= static_cast(o); + float coeff = std::pow(static_cast(elapsed), o) / factorial; + for (size_t i = 0; i < size; i++) { + output[i] += coeff * dY_prev[o][i]; + } + } + } +}; + +struct BlockCacheEntry { + std::vector residual_img; + std::vector residual_txt; + std::vector residual; + std::vector prev_img; + std::vector prev_txt; + std::vector prev_output; + bool has_prev = false; +}; + +struct CacheDitState { + CacheDitConfig config; + bool initialized = false; + + int total_double_blocks = 0; + int total_single_blocks = 0; + size_t hidden_size = 0; + + int current_step = -1; + int total_steps = 0; + int warmup_remaining = 0; + std::vector cached_steps; + int continuous_cached_steps = 0; + float accumulated_residual_diff = 0.0f; + + std::vector double_block_cache; + std::vector single_block_cache; + + std::vector Fn_residual_img; + std::vector Fn_residual_txt; + std::vector prev_Fn_residual_img; + std::vector prev_Fn_residual_txt; + bool has_prev_Fn_residual = false; + + std::vector Bn_buffer_img; + std::vector Bn_buffer_txt; + std::vector Bn_buffer; + bool has_Bn_buffer = false; + + TaylorSeerState taylor_state; + + bool can_cache_this_step = false; + bool is_caching_this_step = false; + + int total_blocks_computed = 0; + int total_blocks_cached = 0; + + void init(const CacheDitConfig& cfg, int num_double_blocks, int num_single_blocks, size_t h_size) { + config = cfg; + total_double_blocks = num_double_blocks; + total_single_blocks = num_single_blocks; + hidden_size = h_size; + + initialized = cfg.dbcache.enabled || cfg.taylorseer.enabled; + + if (!initialized) + return; + + warmup_remaining = cfg.dbcache.max_warmup_steps; + double_block_cache.resize(total_double_blocks); + single_block_cache.resize(total_single_blocks); + + if (cfg.taylorseer.enabled) { + taylor_state.init(cfg.taylorseer.n_derivatives, h_size); + } + + reset_runtime(); + } + + void reset_runtime() { + current_step = -1; + total_steps = 0; + warmup_remaining = config.dbcache.max_warmup_steps; + cached_steps.clear(); + continuous_cached_steps = 0; + accumulated_residual_diff = 0.0f; + + for (auto& entry : double_block_cache) { + entry.residual_img.clear(); + entry.residual_txt.clear(); + entry.prev_img.clear(); + entry.prev_txt.clear(); + entry.has_prev = false; + } + + for (auto& entry : single_block_cache) { + entry.residual.clear(); + entry.prev_output.clear(); + entry.has_prev = false; + } + + Fn_residual_img.clear(); + Fn_residual_txt.clear(); + prev_Fn_residual_img.clear(); + prev_Fn_residual_txt.clear(); + has_prev_Fn_residual = false; + + Bn_buffer_img.clear(); + Bn_buffer_txt.clear(); + Bn_buffer.clear(); + has_Bn_buffer = false; + + taylor_state.reset(); + + can_cache_this_step = false; + is_caching_this_step = false; + + total_blocks_computed = 0; + total_blocks_cached = 0; + } + + bool enabled() const { + return initialized && (config.dbcache.enabled || config.taylorseer.enabled); + } + + void begin_step(int step_index, float sigma = 0.0f) { + if (!enabled()) + return; + if (step_index == current_step) + return; + + current_step = step_index; + total_steps++; + + bool in_warmup = warmup_remaining > 0; + if (in_warmup) { + warmup_remaining--; + } + + bool scm_allows_cache = true; + if (!config.dbcache.steps_computation_mask.empty()) { + if (step_index < static_cast(config.dbcache.steps_computation_mask.size())) { + scm_allows_cache = (config.dbcache.steps_computation_mask[step_index] == 0); + if (!config.dbcache.scm_policy_dynamic && scm_allows_cache) { + can_cache_this_step = true; + is_caching_this_step = false; + return; + } + } + } + + bool max_cached_ok = (config.dbcache.max_cached_steps < 0) || + (static_cast(cached_steps.size()) < config.dbcache.max_cached_steps); + + bool max_cont_ok = (config.dbcache.max_continuous_cached_steps < 0) || + (continuous_cached_steps < config.dbcache.max_continuous_cached_steps); + + bool accum_ok = (config.dbcache.max_accumulated_residual_diff < 0.0f) || + (accumulated_residual_diff < config.dbcache.max_accumulated_residual_diff); + + can_cache_this_step = !in_warmup && scm_allows_cache && max_cached_ok && max_cont_ok && accum_ok && has_prev_Fn_residual; + is_caching_this_step = false; + } + + void end_step(bool was_cached) { + if (was_cached) { + cached_steps.push_back(current_step); + continuous_cached_steps++; + } else { + continuous_cached_steps = 0; + } + } + + static float calculate_residual_diff(const float* prev, const float* curr, size_t size) { + if (size == 0) + return 0.0f; + + float sum_diff = 0.0f; + float sum_abs = 0.0f; + + for (size_t i = 0; i < size; i++) { + sum_diff += std::fabs(prev[i] - curr[i]); + sum_abs += std::fabs(prev[i]); + } + + return sum_diff / (sum_abs + 1e-6f); + } + + static float calculate_residual_diff(const std::vector& prev, const std::vector& curr) { + if (prev.size() != curr.size() || prev.empty()) + return 1.0f; + return calculate_residual_diff(prev.data(), curr.data(), prev.size()); + } + + int get_double_Fn_blocks() const { + return (config.double_Fn_blocks >= 0) ? config.double_Fn_blocks : config.dbcache.Fn_compute_blocks; + } + + int get_double_Bn_blocks() const { + return (config.double_Bn_blocks >= 0) ? config.double_Bn_blocks : config.dbcache.Bn_compute_blocks; + } + + int get_single_Fn_blocks() const { + return (config.single_Fn_blocks >= 0) ? config.single_Fn_blocks : config.dbcache.Fn_compute_blocks; + } + + int get_single_Bn_blocks() const { + return (config.single_Bn_blocks >= 0) ? config.single_Bn_blocks : config.dbcache.Bn_compute_blocks; + } + + bool is_Fn_double_block(int block_idx) const { + return block_idx < get_double_Fn_blocks(); + } + + bool is_Bn_double_block(int block_idx) const { + int Bn = get_double_Bn_blocks(); + return Bn > 0 && block_idx >= (total_double_blocks - Bn); + } + + bool is_Mn_double_block(int block_idx) const { + return !is_Fn_double_block(block_idx) && !is_Bn_double_block(block_idx); + } + + bool is_Fn_single_block(int block_idx) const { + return block_idx < get_single_Fn_blocks(); + } + + bool is_Bn_single_block(int block_idx) const { + int Bn = get_single_Bn_blocks(); + return Bn > 0 && block_idx >= (total_single_blocks - Bn); + } + + bool is_Mn_single_block(int block_idx) const { + return !is_Fn_single_block(block_idx) && !is_Bn_single_block(block_idx); + } + + void store_Fn_residual(const float* img, const float* txt, size_t img_size, size_t txt_size, const float* input_img, const float* input_txt) { + Fn_residual_img.resize(img_size); + Fn_residual_txt.resize(txt_size); + + for (size_t i = 0; i < img_size; i++) { + Fn_residual_img[i] = img[i] - input_img[i]; + } + for (size_t i = 0; i < txt_size; i++) { + Fn_residual_txt[i] = txt[i] - input_txt[i]; + } + } + + bool check_cache_decision() { + if (!can_cache_this_step) { + is_caching_this_step = false; + return false; + } + + if (!has_prev_Fn_residual || prev_Fn_residual_img.empty()) { + is_caching_this_step = false; + return false; + } + + float diff_img = calculate_residual_diff(prev_Fn_residual_img, Fn_residual_img); + float diff_txt = calculate_residual_diff(prev_Fn_residual_txt, Fn_residual_txt); + float diff = (diff_img + diff_txt) / 2.0f; + + if (diff < config.dbcache.residual_diff_threshold) { + is_caching_this_step = true; + accumulated_residual_diff += diff; + return true; + } + + is_caching_this_step = false; + return false; + } + + void update_prev_Fn_residual() { + prev_Fn_residual_img = Fn_residual_img; + prev_Fn_residual_txt = Fn_residual_txt; + has_prev_Fn_residual = !prev_Fn_residual_img.empty(); + } + + void store_double_block_residual(int block_idx, const float* img, const float* txt, size_t img_size, size_t txt_size, const float* prev_img, const float* prev_txt) { + if (block_idx < 0 || block_idx >= static_cast(double_block_cache.size())) + return; + + BlockCacheEntry& entry = double_block_cache[block_idx]; + + entry.residual_img.resize(img_size); + entry.residual_txt.resize(txt_size); + for (size_t i = 0; i < img_size; i++) { + entry.residual_img[i] = img[i] - prev_img[i]; + } + for (size_t i = 0; i < txt_size; i++) { + entry.residual_txt[i] = txt[i] - prev_txt[i]; + } + + entry.prev_img.resize(img_size); + entry.prev_txt.resize(txt_size); + for (size_t i = 0; i < img_size; i++) { + entry.prev_img[i] = img[i]; + } + for (size_t i = 0; i < txt_size; i++) { + entry.prev_txt[i] = txt[i]; + } + entry.has_prev = true; + } + + void apply_double_block_cache(int block_idx, float* img, float* txt, size_t img_size, size_t txt_size) { + if (block_idx < 0 || block_idx >= static_cast(double_block_cache.size())) + return; + + const BlockCacheEntry& entry = double_block_cache[block_idx]; + if (entry.residual_img.size() != img_size || entry.residual_txt.size() != txt_size) + return; + + for (size_t i = 0; i < img_size; i++) { + img[i] += entry.residual_img[i]; + } + for (size_t i = 0; i < txt_size; i++) { + txt[i] += entry.residual_txt[i]; + } + + total_blocks_cached++; + } + + void store_single_block_residual(int block_idx, const float* output, size_t size, const float* input) { + if (block_idx < 0 || block_idx >= static_cast(single_block_cache.size())) + return; + + BlockCacheEntry& entry = single_block_cache[block_idx]; + + entry.residual.resize(size); + for (size_t i = 0; i < size; i++) { + entry.residual[i] = output[i] - input[i]; + } + + entry.prev_output.resize(size); + for (size_t i = 0; i < size; i++) { + entry.prev_output[i] = output[i]; + } + entry.has_prev = true; + } + + void apply_single_block_cache(int block_idx, float* output, size_t size) { + if (block_idx < 0 || block_idx >= static_cast(single_block_cache.size())) + return; + + const BlockCacheEntry& entry = single_block_cache[block_idx]; + if (entry.residual.size() != size) + return; + + for (size_t i = 0; i < size; i++) { + output[i] += entry.residual[i]; + } + + total_blocks_cached++; + } + + void store_Bn_buffer(const float* img, const float* txt, size_t img_size, size_t txt_size, const float* Bn_start_img, const float* Bn_start_txt) { + Bn_buffer_img.resize(img_size); + Bn_buffer_txt.resize(txt_size); + + for (size_t i = 0; i < img_size; i++) { + Bn_buffer_img[i] = img[i] - Bn_start_img[i]; + } + for (size_t i = 0; i < txt_size; i++) { + Bn_buffer_txt[i] = txt[i] - Bn_start_txt[i]; + } + has_Bn_buffer = true; + } + + void apply_Bn_buffer(float* img, float* txt, size_t img_size, size_t txt_size) { + if (!has_Bn_buffer) + return; + if (Bn_buffer_img.size() != img_size || Bn_buffer_txt.size() != txt_size) + return; + + for (size_t i = 0; i < img_size; i++) { + img[i] += Bn_buffer_img[i]; + } + for (size_t i = 0; i < txt_size; i++) { + txt[i] += Bn_buffer_txt[i]; + } + } + + void taylor_update(const float* hidden_state, size_t size) { + if (!config.taylorseer.enabled) + return; + taylor_state.update_derivatives(hidden_state, size, current_step); + } + + bool taylor_can_approximate() const { + return config.taylorseer.enabled && taylor_state.can_approximate(); + } + + void taylor_approximate(float* output, size_t size) { + if (!config.taylorseer.enabled) + return; + taylor_state.approximate(output, size, current_step); + } + + bool should_use_taylor_this_step() const { + if (!config.taylorseer.enabled) + return false; + if (current_step < config.taylorseer.max_warmup_steps) + return false; + + int interval = config.taylorseer.skip_interval_steps; + if (interval <= 0) + interval = 1; + + return (current_step % (interval + 1)) != 0; + } + + void log_metrics() const { + if (!enabled()) + return; + + int total_blocks = total_blocks_computed + total_blocks_cached; + float cache_ratio = (total_blocks > 0) ? (static_cast(total_blocks_cached) / total_blocks * 100.0f) : 0.0f; + + float step_cache_ratio = (total_steps > 0) ? (static_cast(cached_steps.size()) / total_steps * 100.0f) : 0.0f; + + LOG_INFO("CacheDIT: steps_cached=%zu/%d (%.1f%%), blocks_cached=%d/%d (%.1f%%), accum_diff=%.4f", + cached_steps.size(), total_steps, step_cache_ratio, + total_blocks_cached, total_blocks, cache_ratio, + accumulated_residual_diff); + } + + std::string get_summary() const { + char buf[256]; + snprintf(buf, sizeof(buf), + "CacheDIT[thresh=%.2f]: cached %zu/%d steps, %d/%d blocks", + config.dbcache.residual_diff_threshold, + cached_steps.size(), total_steps, + total_blocks_cached, total_blocks_computed + total_blocks_cached); + return std::string(buf); + } +}; + +inline std::vector parse_scm_mask(const std::string& mask_str) { + std::vector mask; + if (mask_str.empty()) + return mask; + + size_t pos = 0; + size_t start = 0; + while ((pos = mask_str.find(',', start)) != std::string::npos) { + std::string token = mask_str.substr(start, pos - start); + mask.push_back(std::stoi(token)); + start = pos + 1; + } + if (start < mask_str.length()) { + mask.push_back(std::stoi(mask_str.substr(start))); + } + + return mask; +} + +inline std::vector generate_scm_mask( + const std::vector& compute_bins, + const std::vector& cache_bins, + int total_steps) { + std::vector mask; + size_t c_idx = 0, cache_idx = 0; + + while (static_cast(mask.size()) < total_steps) { + if (c_idx < compute_bins.size()) { + for (int i = 0; i < compute_bins[c_idx] && static_cast(mask.size()) < total_steps; i++) { + mask.push_back(1); + } + c_idx++; + } + if (cache_idx < cache_bins.size()) { + for (int i = 0; i < cache_bins[cache_idx] && static_cast(mask.size()) < total_steps; i++) { + mask.push_back(0); + } + cache_idx++; + } + if (c_idx >= compute_bins.size() && cache_idx >= cache_bins.size()) + break; + } + + if (!mask.empty()) { + mask.back() = 1; + } + + return mask; +} + +inline std::vector get_scm_preset(const std::string& preset, int total_steps) { + struct Preset { + std::vector compute_bins; + std::vector cache_bins; + }; + + Preset slow = {{8, 3, 3, 2, 1, 1}, {1, 2, 2, 2, 3}}; + Preset medium = {{6, 2, 2, 2, 2, 1}, {1, 3, 3, 3, 3}}; + Preset fast = {{6, 1, 1, 1, 1, 1}, {1, 3, 4, 5, 4}}; + Preset ultra = {{4, 1, 1, 1, 1}, {2, 5, 6, 7}}; + + Preset* p = nullptr; + if (preset == "slow" || preset == "s" || preset == "S") + p = &slow; + else if (preset == "medium" || preset == "m" || preset == "M") + p = &medium; + else if (preset == "fast" || preset == "f" || preset == "F") + p = &fast; + else if (preset == "ultra" || preset == "u" || preset == "U") + p = &ultra; + else + return {}; + + if (total_steps != 28 && total_steps > 0) { + float scale = static_cast(total_steps) / 28.0f; + std::vector scaled_compute, scaled_cache; + + for (int v : p->compute_bins) { + scaled_compute.push_back(std::max(1, static_cast(v * scale + 0.5f))); + } + for (int v : p->cache_bins) { + scaled_cache.push_back(std::max(1, static_cast(v * scale + 0.5f))); + } + + return generate_scm_mask(scaled_compute, scaled_cache, total_steps); + } + + return generate_scm_mask(p->compute_bins, p->cache_bins, total_steps); +} + +inline float get_preset_threshold(const std::string& preset) { + if (preset == "slow" || preset == "s" || preset == "S") + return 0.20f; + if (preset == "medium" || preset == "m" || preset == "M") + return 0.25f; + if (preset == "fast" || preset == "f" || preset == "F") + return 0.30f; + if (preset == "ultra" || preset == "u" || preset == "U") + return 0.34f; + return 0.08f; +} + +inline int get_preset_warmup(const std::string& preset) { + if (preset == "slow" || preset == "s" || preset == "S") + return 8; + if (preset == "medium" || preset == "m" || preset == "M") + return 6; + if (preset == "fast" || preset == "f" || preset == "F") + return 6; + if (preset == "ultra" || preset == "u" || preset == "U") + return 4; + return 8; +} + +inline int get_preset_Fn(const std::string& preset) { + if (preset == "slow" || preset == "s" || preset == "S") + return 8; + if (preset == "medium" || preset == "m" || preset == "M") + return 8; + if (preset == "fast" || preset == "f" || preset == "F") + return 6; + if (preset == "ultra" || preset == "u" || preset == "U") + return 4; + return 8; +} + +inline int get_preset_Bn(const std::string& preset) { + (void)preset; + return 0; +} + +inline void parse_dbcache_options(const std::string& opts, DBCacheConfig& cfg) { + if (opts.empty()) + return; + + int Fn = 8, Bn = 0, warmup = 8, max_cached = -1, max_cont = -1; + float thresh = 0.08f; + + sscanf(opts.c_str(), "%d,%d,%f,%d,%d,%d", + &Fn, &Bn, &thresh, &warmup, &max_cached, &max_cont); + + cfg.Fn_compute_blocks = Fn; + cfg.Bn_compute_blocks = Bn; + cfg.residual_diff_threshold = thresh; + cfg.max_warmup_steps = warmup; + cfg.max_cached_steps = max_cached; + cfg.max_continuous_cached_steps = max_cont; +} + +inline void parse_taylorseer_options(const std::string& opts, TaylorSeerConfig& cfg) { + if (opts.empty()) + return; + + int n_deriv = 1, warmup = 2, interval = 1; + sscanf(opts.c_str(), "%d,%d,%d", &n_deriv, &warmup, &interval); + + cfg.n_derivatives = n_deriv; + cfg.max_warmup_steps = warmup; + cfg.skip_interval_steps = interval; +} + +struct CacheDitConditionState { + DBCacheConfig config; + TaylorSeerConfig taylor_config; + bool initialized = false; + + int current_step_index = -1; + bool step_active = false; + bool skip_current_step = false; + bool initial_step = true; + int warmup_remaining = 0; + std::vector cached_steps; + int continuous_cached_steps = 0; + float accumulated_residual_diff = 0.0f; + int total_steps_skipped = 0; + + const void* anchor_condition = nullptr; + + struct CacheEntry { + std::vector diff; + std::vector prev_input; + std::vector prev_output; + bool has_prev = false; + }; + std::unordered_map cache_diffs; + + TaylorSeerState taylor_state; + + float start_sigma = std::numeric_limits::max(); + float end_sigma = 0.0f; + + void reset_runtime() { + current_step_index = -1; + step_active = false; + skip_current_step = false; + initial_step = true; + warmup_remaining = config.max_warmup_steps; + cached_steps.clear(); + continuous_cached_steps = 0; + accumulated_residual_diff = 0.0f; + total_steps_skipped = 0; + anchor_condition = nullptr; + cache_diffs.clear(); + taylor_state.reset(); + } + + void init(const DBCacheConfig& dbcfg, const TaylorSeerConfig& tcfg) { + config = dbcfg; + taylor_config = tcfg; + initialized = dbcfg.enabled || tcfg.enabled; + reset_runtime(); + + if (taylor_config.enabled) { + taylor_state.init(taylor_config.n_derivatives, 0); + } + } + + void set_sigmas(const std::vector& sigmas) { + if (!initialized || sigmas.size() < 2) + return; + + float start_percent = 0.15f; + float end_percent = 0.95f; + + size_t n_steps = sigmas.size() - 1; + size_t start_step = static_cast(start_percent * n_steps); + size_t end_step = static_cast(end_percent * n_steps); + + if (start_step >= n_steps) + start_step = n_steps - 1; + if (end_step >= n_steps) + end_step = n_steps - 1; + + start_sigma = sigmas[start_step]; + end_sigma = sigmas[end_step]; + + if (start_sigma < end_sigma) { + std::swap(start_sigma, end_sigma); + } + } + + bool enabled() const { + return initialized && (config.enabled || taylor_config.enabled); + } + + void begin_step(int step_index, float sigma) { + if (!enabled()) + return; + if (step_index == current_step_index) + return; + + current_step_index = step_index; + skip_current_step = false; + step_active = false; + + if (sigma > start_sigma) + return; + if (!(sigma > end_sigma)) + return; + + step_active = true; + + if (warmup_remaining > 0) { + warmup_remaining--; + return; + } + + if (!config.steps_computation_mask.empty()) { + if (step_index < static_cast(config.steps_computation_mask.size())) { + if (config.steps_computation_mask[step_index] == 1) { + return; + } + } + } + + if (config.max_cached_steps >= 0 && + static_cast(cached_steps.size()) >= config.max_cached_steps) { + return; + } + + if (config.max_continuous_cached_steps >= 0 && + continuous_cached_steps >= config.max_continuous_cached_steps) { + return; + } + } + + bool step_is_active() const { + return enabled() && step_active; + } + + bool is_step_skipped() const { + return enabled() && step_active && skip_current_step; + } + + bool has_cache(const void* cond) const { + auto it = cache_diffs.find(cond); + return it != cache_diffs.end() && !it->second.diff.empty(); + } + + void update_cache(const void* cond, const float* input, const float* output, size_t size) { + CacheEntry& entry = cache_diffs[cond]; + entry.diff.resize(size); + for (size_t i = 0; i < size; i++) { + entry.diff[i] = output[i] - input[i]; + } + + entry.prev_input.resize(size); + entry.prev_output.resize(size); + for (size_t i = 0; i < size; i++) { + entry.prev_input[i] = input[i]; + entry.prev_output[i] = output[i]; + } + entry.has_prev = true; + } + + void apply_cache(const void* cond, const float* input, float* output, size_t size) { + auto it = cache_diffs.find(cond); + if (it == cache_diffs.end() || it->second.diff.empty()) + return; + if (it->second.diff.size() != size) + return; + + for (size_t i = 0; i < size; i++) { + output[i] = input[i] + it->second.diff[i]; + } + } + + bool before_condition(const void* cond, struct ggml_tensor* input, struct ggml_tensor* output, float sigma, int step_index) { + if (!enabled() || step_index < 0) + return false; + + if (step_index != current_step_index) { + begin_step(step_index, sigma); + } + + if (!step_active) + return false; + + if (initial_step) { + anchor_condition = cond; + initial_step = false; + } + + bool is_anchor = (cond == anchor_condition); + + if (skip_current_step) { + if (has_cache(cond)) { + apply_cache(cond, (float*)input->data, (float*)output->data, + static_cast(ggml_nelements(output))); + return true; + } + return false; + } + + if (!is_anchor) + return false; + + auto it = cache_diffs.find(cond); + if (it == cache_diffs.end() || !it->second.has_prev) + return false; + + size_t ne = static_cast(ggml_nelements(input)); + if (it->second.prev_input.size() != ne) + return false; + + float* input_data = (float*)input->data; + float diff = CacheDitState::calculate_residual_diff( + it->second.prev_input.data(), input_data, ne); + + float effective_threshold = config.residual_diff_threshold; + if (config.Fn_compute_blocks > 0) { + float fn_confidence = 1.0f + 0.02f * (config.Fn_compute_blocks - 8); + fn_confidence = std::max(0.5f, std::min(2.0f, fn_confidence)); + effective_threshold *= fn_confidence; + } + if (config.Bn_compute_blocks > 0) { + float bn_quality = 1.0f - 0.03f * config.Bn_compute_blocks; + bn_quality = std::max(0.5f, std::min(1.0f, bn_quality)); + effective_threshold *= bn_quality; + } + + if (diff < effective_threshold) { + skip_current_step = true; + total_steps_skipped++; + cached_steps.push_back(current_step_index); + continuous_cached_steps++; + accumulated_residual_diff += diff; + apply_cache(cond, input_data, (float*)output->data, ne); + return true; + } + + continuous_cached_steps = 0; + return false; + } + + void after_condition(const void* cond, struct ggml_tensor* input, struct ggml_tensor* output) { + if (!step_is_active()) + return; + + size_t ne = static_cast(ggml_nelements(output)); + update_cache(cond, (float*)input->data, (float*)output->data, ne); + + if (cond == anchor_condition && taylor_config.enabled) { + taylor_state.update_derivatives((float*)output->data, ne, current_step_index); + } + } + + void log_metrics() const { + if (!enabled()) + return; + + LOG_INFO("CacheDIT: steps_skipped=%d/%d (%.1f%%), accum_residual_diff=%.4f", + total_steps_skipped, + current_step_index + 1, + (current_step_index > 0) ? (100.0f * total_steps_skipped / (current_step_index + 1)) : 0.0f, + accumulated_residual_diff); + } +}; + +#endif diff --git a/docs/caching.md b/docs/caching.md new file mode 100644 index 000000000..7b4be3ce0 --- /dev/null +++ b/docs/caching.md @@ -0,0 +1,126 @@ +## Caching + +Caching methods accelerate diffusion inference by reusing intermediate computations when changes between steps are small. + +### Cache Modes + +| Mode | Target | Description | +|------|--------|-------------| +| `ucache` | UNET models | Condition-level caching with error tracking | +| `easycache` | DiT models | Condition-level cache | +| `dbcache` | DiT models | Block-level L1 residual threshold | +| `taylorseer` | DiT models | Taylor series approximation | +| `cache-dit` | DiT models | Combined DBCache + TaylorSeer | + +### UCache (UNET Models) + +UCache caches the residual difference (output - input) and reuses it when input changes are below threshold. + +```bash +sd-cli -m model.safetensors -p "a cat" --cache-mode ucache --cache-option "threshold=1.5" +``` + +#### Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `threshold` | Error threshold for reuse decision | 1.0 | +| `start` | Start caching at this percent of steps | 0.15 | +| `end` | Stop caching at this percent of steps | 0.95 | +| `decay` | Error decay rate (0-1) | 1.0 | +| `relative` | Scale threshold by output norm (0/1) | 1 | +| `reset` | Reset error after computing (0/1) | 1 | + +#### Reset Parameter + +The `reset` parameter controls error accumulation behavior: + +- `reset=1` (default): Resets accumulated error after each computed step. More aggressive caching, works well with most samplers. +- `reset=0`: Keeps error accumulated. More conservative, recommended for `euler_a` sampler. + +### EasyCache (DiT Models) + +Condition-level caching for DiT models. Caches and reuses outputs when input changes are below threshold. + +```bash +--cache-mode easycache --cache-option "threshold=0.3" +``` + +#### Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `threshold` | Input change threshold for reuse | 0.2 | +| `start` | Start caching at this percent of steps | 0.15 | +| `end` | Stop caching at this percent of steps | 0.95 | + +### Cache-DIT (DiT Models) + +For DiT models like FLUX and QWEN, use block-level caching modes. + +#### DBCache + +Caches blocks based on L1 residual difference threshold: + +```bash +--cache-mode dbcache --cache-option "threshold=0.25,warmup=4" +``` + +#### TaylorSeer + +Uses Taylor series approximation to predict block outputs: + +```bash +--cache-mode taylorseer +``` + +#### Cache-DIT (Combined) + +Combines DBCache and TaylorSeer: + +```bash +--cache-mode cache-dit --cache-preset fast +``` + +#### Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `Fn` | Front blocks to always compute | 8 | +| `Bn` | Back blocks to always compute | 0 | +| `threshold` | L1 residual difference threshold | 0.08 | +| `warmup` | Steps before caching starts | 8 | + +#### Presets + +Available presets: `slow`, `medium`, `fast`, `ultra` (or `s`, `m`, `f`, `u`). + +```bash +--cache-mode cache-dit --cache-preset fast +``` + +#### SCM Options + +Steps Computation Mask controls which steps can be cached: + +```bash +--scm-mask "1,1,1,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1" +``` + +Mask values: `1` = compute, `0` = can cache. + +| Policy | Description | +|--------|-------------| +| `dynamic` | Check threshold before caching | +| `static` | Always cache on cacheable steps | + +```bash +--scm-policy dynamic +``` + +### Performance Tips + +- Start with default thresholds and adjust based on output quality +- Lower threshold = better quality, less speedup +- Higher threshold = more speedup, potential quality loss +- More steps generally means more caching opportunities diff --git a/examples/cli/README.md b/examples/cli/README.md index d6cb6aa3b..0617a4637 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -127,5 +127,12 @@ Generation Options: --skip-layers layers to skip for SLG steps (default: [7,8,9]) --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) -r, --ref-image reference image for Flux Kontext models (can be used multiple times) - --easycache enable EasyCache for DiT models with optional "threshold,start_percent,end_percent" (default: 0.2,0.15,0.95) + --cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level) + --cache-option named cache params (key=value format, comma-separated): + - easycache/ucache: threshold=,start=,end=,decay=,relative=,reset= + - dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup= + Examples: "threshold=0.25" or "threshold=1.5,reset=0" + --cache-preset cache-dit preset: 'slow'/'s', 'medium'/'m', 'fast'/'f', 'ultra'/'u' + --scm-mask SCM steps mask: comma-separated 0/1 (1=compute, 0=can cache) + --scm-policy SCM policy: 'dynamic' (default) or 'static' ``` diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 708dbb104..7fe3b7692 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -617,7 +617,7 @@ int main(int argc, const char* argv[]) { gen_params.pm_style_strength, }, // pm_params ctx_params.vae_tiling_params, - gen_params.easycache_params, + gen_params.cache_params, }; results = generate_image(sd_ctx, &img_gen_params); @@ -642,7 +642,7 @@ int main(int argc, const char* argv[]) { gen_params.seed, gen_params.video_frames, gen_params.vace_strength, - gen_params.easycache_params, + gen_params.cache_params, }; results = generate_video(sd_ctx, &vid_gen_params, &num_results); diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 5168730aa..868b06e92 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -1018,8 +1018,12 @@ struct SDGenerationParams { std::vector custom_sigmas; - std::string easycache_option; - sd_easycache_params_t easycache_params; + std::string cache_mode; + std::string cache_option; + std::string cache_preset; + std::string scm_mask; + bool scm_policy_dynamic = true; + sd_cache_params_t cache_params{}; float moe_boundary = 0.875f; int video_frames = 1; @@ -1381,36 +1385,64 @@ struct SDGenerationParams { return 1; }; - auto on_easycache_arg = [&](int argc, const char** argv, int index) { - const std::string default_values = "0.2,0.15,0.95"; - auto looks_like_value = [](const std::string& token) { - if (token.empty()) { - return false; - } - if (token[0] != '-') { - return true; - } - if (token.size() == 1) { - return false; - } - unsigned char next = static_cast(token[1]); - return std::isdigit(next) || token[1] == '.'; - }; - - std::string option_value; - int consumed = 0; - if (index + 1 < argc) { - std::string next_arg = argv[index + 1]; - if (looks_like_value(next_arg)) { - option_value = argv_to_utf8(index + 1, argv); - consumed = 1; - } + auto on_cache_mode_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; } - if (option_value.empty()) { - option_value = default_values; + cache_mode = argv_to_utf8(index, argv); + if (cache_mode != "easycache" && cache_mode != "ucache" && + cache_mode != "dbcache" && cache_mode != "taylorseer" && cache_mode != "cache-dit") { + fprintf(stderr, "error: invalid cache mode '%s', must be 'easycache', 'ucache', 'dbcache', 'taylorseer', or 'cache-dit'\n", cache_mode.c_str()); + return -1; } - easycache_option = option_value; - return consumed; + return 1; + }; + + auto on_cache_option_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + cache_option = argv_to_utf8(index, argv); + return 1; + }; + + auto on_scm_mask_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + scm_mask = argv_to_utf8(index, argv); + return 1; + }; + + auto on_scm_policy_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + std::string policy = argv_to_utf8(index, argv); + if (policy == "dynamic") { + scm_policy_dynamic = true; + } else if (policy == "static") { + scm_policy_dynamic = false; + } else { + fprintf(stderr, "error: invalid scm policy '%s', must be 'dynamic' or 'static'\n", policy.c_str()); + return -1; + } + return 1; + }; + + auto on_cache_preset_arg = [&](int argc, const char** argv, int index) { + if (++index >= argc) { + return -1; + } + cache_preset = argv_to_utf8(index, argv); + if (cache_preset != "slow" && cache_preset != "s" && cache_preset != "S" && + cache_preset != "medium" && cache_preset != "m" && cache_preset != "M" && + cache_preset != "fast" && cache_preset != "f" && cache_preset != "F" && + cache_preset != "ultra" && cache_preset != "u" && cache_preset != "U") { + fprintf(stderr, "error: invalid cache preset '%s', must be 'slow'/'s', 'medium'/'m', 'fast'/'f', or 'ultra'/'u'\n", cache_preset.c_str()); + return -1; + } + return 1; }; options.manual_options = { @@ -1449,9 +1481,25 @@ struct SDGenerationParams { "reference image for Flux Kontext models (can be used multiple times)", on_ref_image_arg}, {"", - "--easycache", - "enable EasyCache for DiT models with optional \"threshold,start_percent,end_percent\" (default: 0.2,0.15,0.95)", - on_easycache_arg}, + "--cache-mode", + "caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level)", + on_cache_mode_arg}, + {"", + "--cache-option", + "named cache params (key=value format, comma-separated):\n - easycache/ucache: threshold=,start=,end=,decay=,relative=,reset=\n - dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup=\n Examples: \"threshold=0.25\" or \"threshold=1.5,reset=0\"", + on_cache_option_arg}, + {"", + "--cache-preset", + "cache-dit preset: 'slow'/'s', 'medium'/'m', 'fast'/'f', 'ultra'/'u'", + on_cache_preset_arg}, + {"", + "--scm-mask", + "SCM steps mask for cache-dit: comma-separated 0/1 (e.g., \"1,1,1,0,0,1,0,0,1,0\") - 1=compute, 0=can cache", + on_scm_mask_arg}, + {"", + "--scm-policy", + "SCM policy: 'dynamic' (default) or 'static'", + on_scm_policy_arg}, }; @@ -1494,7 +1542,10 @@ struct SDGenerationParams { load_if_exists("prompt", prompt); load_if_exists("negative_prompt", negative_prompt); - load_if_exists("easycache_option", easycache_option); + load_if_exists("cache_mode", cache_mode); + load_if_exists("cache_option", cache_option); + load_if_exists("cache_preset", cache_preset); + load_if_exists("scm_mask", scm_mask); load_if_exists("clip_skip", clip_skip); load_if_exists("width", width); @@ -1634,57 +1685,118 @@ struct SDGenerationParams { return false; } - if (!easycache_option.empty()) { - float values[3] = {0.0f, 0.0f, 0.0f}; - std::stringstream ss(easycache_option); + sd_cache_params_init(&cache_params); + + auto parse_named_params = [&](const std::string& opt_str) -> bool { + std::stringstream ss(opt_str); std::string token; - int idx = 0; while (std::getline(ss, token, ',')) { - auto trim = [](std::string& s) { - const char* whitespace = " \t\r\n"; - auto start = s.find_first_not_of(whitespace); - if (start == std::string::npos) { - s.clear(); - return; - } - auto end = s.find_last_not_of(whitespace); - s = s.substr(start, end - start + 1); - }; - trim(token); - if (token.empty()) { - LOG_ERROR("error: invalid easycache option '%s'", easycache_option.c_str()); - return false; - } - if (idx >= 3) { - LOG_ERROR("error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); + size_t eq_pos = token.find('='); + if (eq_pos == std::string::npos) { + LOG_ERROR("error: cache option '%s' missing '=' separator", token.c_str()); return false; } + std::string key = token.substr(0, eq_pos); + std::string val = token.substr(eq_pos + 1); try { - values[idx] = std::stof(token); + if (key == "threshold") { + if (cache_mode == "easycache" || cache_mode == "ucache") { + cache_params.reuse_threshold = std::stof(val); + } else { + cache_params.residual_diff_threshold = std::stof(val); + } + } else if (key == "start") { + cache_params.start_percent = std::stof(val); + } else if (key == "end") { + cache_params.end_percent = std::stof(val); + } else if (key == "decay") { + cache_params.error_decay_rate = std::stof(val); + } else if (key == "relative") { + cache_params.use_relative_threshold = (std::stof(val) != 0.0f); + } else if (key == "reset") { + cache_params.reset_error_on_compute = (std::stof(val) != 0.0f); + } else if (key == "Fn" || key == "fn") { + cache_params.Fn_compute_blocks = std::stoi(val); + } else if (key == "Bn" || key == "bn") { + cache_params.Bn_compute_blocks = std::stoi(val); + } else if (key == "warmup") { + cache_params.max_warmup_steps = std::stoi(val); + } else { + LOG_ERROR("error: unknown cache parameter '%s'", key.c_str()); + return false; + } } catch (const std::exception&) { - LOG_ERROR("error: invalid easycache value '%s'", token.c_str()); + LOG_ERROR("error: invalid value '%s' for parameter '%s'", val.c_str(), key.c_str()); return false; } - idx++; } - if (idx != 3) { - LOG_ERROR("error: easycache expects exactly 3 comma-separated values (threshold,start,end)\n"); - return false; + return true; + }; + + if (!cache_mode.empty()) { + if (cache_mode == "easycache") { + cache_params.mode = SD_CACHE_EASYCACHE; + cache_params.reuse_threshold = 0.2f; + cache_params.start_percent = 0.15f; + cache_params.end_percent = 0.95f; + cache_params.error_decay_rate = 1.0f; + cache_params.use_relative_threshold = true; + cache_params.reset_error_on_compute = true; + } else if (cache_mode == "ucache") { + cache_params.mode = SD_CACHE_UCACHE; + cache_params.reuse_threshold = 1.0f; + cache_params.start_percent = 0.15f; + cache_params.end_percent = 0.95f; + cache_params.error_decay_rate = 1.0f; + cache_params.use_relative_threshold = true; + cache_params.reset_error_on_compute = true; + } else if (cache_mode == "dbcache") { + cache_params.mode = SD_CACHE_DBCACHE; + cache_params.Fn_compute_blocks = 8; + cache_params.Bn_compute_blocks = 0; + cache_params.residual_diff_threshold = 0.08f; + cache_params.max_warmup_steps = 8; + } else if (cache_mode == "taylorseer") { + cache_params.mode = SD_CACHE_TAYLORSEER; + cache_params.Fn_compute_blocks = 8; + cache_params.Bn_compute_blocks = 0; + cache_params.residual_diff_threshold = 0.08f; + cache_params.max_warmup_steps = 8; + } else if (cache_mode == "cache-dit") { + cache_params.mode = SD_CACHE_CACHE_DIT; + cache_params.Fn_compute_blocks = 8; + cache_params.Bn_compute_blocks = 0; + cache_params.residual_diff_threshold = 0.08f; + cache_params.max_warmup_steps = 8; } - if (values[0] < 0.0f) { - LOG_ERROR("error: easycache threshold must be non-negative\n"); - return false; + + if (!cache_option.empty()) { + if (!parse_named_params(cache_option)) { + return false; + } } - if (values[1] < 0.0f || values[1] >= 1.0f || values[2] <= 0.0f || values[2] > 1.0f || values[1] >= values[2]) { - LOG_ERROR("error: easycache start/end percents must satisfy 0.0 <= start < end <= 1.0\n"); - return false; + + if (cache_mode == "easycache" || cache_mode == "ucache") { + if (cache_params.reuse_threshold < 0.0f) { + LOG_ERROR("error: cache threshold must be non-negative"); + return false; + } + if (cache_params.start_percent < 0.0f || cache_params.start_percent >= 1.0f || + cache_params.end_percent <= 0.0f || cache_params.end_percent > 1.0f || + cache_params.start_percent >= cache_params.end_percent) { + LOG_ERROR("error: cache start/end percents must satisfy 0.0 <= start < end <= 1.0"); + return false; + } + } + } + + if (cache_params.mode == SD_CACHE_DBCACHE || + cache_params.mode == SD_CACHE_TAYLORSEER || + cache_params.mode == SD_CACHE_CACHE_DIT) { + if (!scm_mask.empty()) { + cache_params.scm_mask = scm_mask.c_str(); } - easycache_params.enabled = true; - easycache_params.reuse_threshold = values[0]; - easycache_params.start_percent = values[1]; - easycache_params.end_percent = values[2]; - } else { - easycache_params.enabled = false; + cache_params.scm_policy_dynamic = scm_policy_dynamic; } sample_params.guidance.slg.layers = skip_layers.data(); @@ -1786,12 +1898,13 @@ struct SDGenerationParams { << " high_noise_skip_layers: " << vec_to_string(high_noise_skip_layers) << ",\n" << " high_noise_sample_params: " << high_noise_sample_params_str << ",\n" << " custom_sigmas: " << vec_to_string(custom_sigmas) << ",\n" - << " easycache_option: \"" << easycache_option << "\",\n" - << " easycache: " - << (easycache_params.enabled ? "enabled" : "disabled") - << " (threshold=" << easycache_params.reuse_threshold - << ", start=" << easycache_params.start_percent - << ", end=" << easycache_params.end_percent << "),\n" + << " cache_mode: \"" << cache_mode << "\",\n" + << " cache_option: \"" << cache_option << "\",\n" + << " cache: " + << (cache_params.mode != SD_CACHE_DISABLED ? "enabled" : "disabled") + << " (threshold=" << cache_params.reuse_threshold + << ", start=" << cache_params.start_percent + << ", end=" << cache_params.end_percent << "),\n" << " moe_boundary: " << moe_boundary << ",\n" << " video_frames: " << video_frames << ",\n" << " fps: " << fps << ",\n" diff --git a/examples/server/main.cpp b/examples/server/main.cpp index 39359fbbe..5c951c075 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -432,7 +432,7 @@ int main(int argc, const char** argv) { gen_params.pm_style_strength, }, // pm_params ctx_params.vae_tiling_params, - gen_params.easycache_params, + gen_params.cache_params, }; sd_image_t* results = nullptr; @@ -645,7 +645,7 @@ int main(int argc, const char** argv) { gen_params.pm_style_strength, }, // pm_params ctx_params.vae_tiling_params, - gen_params.easycache_params, + gen_params.cache_params, }; sd_image_t* results = nullptr; diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 24516a9bb..6c783e1d0 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -7,6 +7,7 @@ #include "stable-diffusion.h" #include "util.h" +#include "cache_dit.hpp" #include "conditioner.hpp" #include "control.hpp" #include "denoiser.hpp" @@ -16,6 +17,7 @@ #include "lora.hpp" #include "pmid.hpp" #include "tae.hpp" +#include "ucache.hpp" #include "vae.hpp" #include "latent-preview.h" @@ -1525,12 +1527,12 @@ class StableDiffusionGGML { const std::vector& sigmas, int start_merge_step, SDCondition id_cond, - std::vector ref_latents = {}, - bool increase_ref_index = false, - ggml_tensor* denoise_mask = nullptr, - ggml_tensor* vace_context = nullptr, - float vace_strength = 1.f, - const sd_easycache_params_t* easycache_params = nullptr) { + std::vector ref_latents = {}, + bool increase_ref_index = false, + ggml_tensor* denoise_mask = nullptr, + ggml_tensor* vace_context = nullptr, + float vace_strength = 1.f, + const sd_cache_params_t* cache_params = nullptr) { if (shifted_timestep > 0 && !sd_version_is_sdxl(version)) { LOG_WARN("timestep shifting is only supported for SDXL models!"); shifted_timestep = 0; @@ -1558,31 +1560,40 @@ class StableDiffusionGGML { } EasyCacheState easycache_state; + UCacheState ucache_state; + CacheDitConditionState cachedit_state; bool easycache_enabled = false; - if (easycache_params != nullptr && easycache_params->enabled) { - bool easycache_supported = sd_version_is_dit(version); - if (!easycache_supported) { - LOG_WARN("EasyCache requested but not supported for this model type"); - } else { - EasyCacheConfig easycache_config; - easycache_config.enabled = true; - easycache_config.reuse_threshold = std::max(0.0f, easycache_params->reuse_threshold); - easycache_config.start_percent = easycache_params->start_percent; - easycache_config.end_percent = easycache_params->end_percent; - bool percent_valid = easycache_config.start_percent >= 0.0f && - easycache_config.start_percent < 1.0f && - easycache_config.end_percent > 0.0f && - easycache_config.end_percent <= 1.0f && - easycache_config.start_percent < easycache_config.end_percent; - if (!percent_valid) { - LOG_WARN("EasyCache disabled due to invalid percent range (start=%.3f, end=%.3f)", - easycache_config.start_percent, - easycache_config.end_percent); + bool ucache_enabled = false; + bool cachedit_enabled = false; + + if (cache_params != nullptr && cache_params->mode != SD_CACHE_DISABLED) { + bool percent_valid = true; + if (cache_params->mode == SD_CACHE_EASYCACHE || cache_params->mode == SD_CACHE_UCACHE) { + percent_valid = cache_params->start_percent >= 0.0f && + cache_params->start_percent < 1.0f && + cache_params->end_percent > 0.0f && + cache_params->end_percent <= 1.0f && + cache_params->start_percent < cache_params->end_percent; + } + + if (!percent_valid) { + LOG_WARN("Cache disabled due to invalid percent range (start=%.3f, end=%.3f)", + cache_params->start_percent, + cache_params->end_percent); + } else if (cache_params->mode == SD_CACHE_EASYCACHE) { + bool easycache_supported = sd_version_is_dit(version); + if (!easycache_supported) { + LOG_WARN("EasyCache requested but not supported for this model type"); } else { + EasyCacheConfig easycache_config; + easycache_config.enabled = true; + easycache_config.reuse_threshold = std::max(0.0f, cache_params->reuse_threshold); + easycache_config.start_percent = cache_params->start_percent; + easycache_config.end_percent = cache_params->end_percent; easycache_state.init(easycache_config, denoiser.get()); if (easycache_state.enabled()) { easycache_enabled = true; - LOG_INFO("EasyCache enabled - threshold: %.3f, start_percent: %.2f, end_percent: %.2f", + LOG_INFO("EasyCache enabled - threshold: %.3f, start: %.2f, end: %.2f", easycache_config.reuse_threshold, easycache_config.start_percent, easycache_config.end_percent); @@ -1590,9 +1601,84 @@ class StableDiffusionGGML { LOG_WARN("EasyCache requested but could not be initialized for this run"); } } + } else if (cache_params->mode == SD_CACHE_UCACHE) { + bool ucache_supported = sd_version_is_unet(version); + if (!ucache_supported) { + LOG_WARN("UCache requested but not supported for this model type (only UNET models)"); + } else { + UCacheConfig ucache_config; + ucache_config.enabled = true; + ucache_config.reuse_threshold = std::max(0.0f, cache_params->reuse_threshold); + ucache_config.start_percent = cache_params->start_percent; + ucache_config.end_percent = cache_params->end_percent; + ucache_config.error_decay_rate = std::max(0.0f, std::min(1.0f, cache_params->error_decay_rate)); + ucache_config.use_relative_threshold = cache_params->use_relative_threshold; + ucache_config.reset_error_on_compute = cache_params->reset_error_on_compute; + ucache_state.init(ucache_config, denoiser.get()); + if (ucache_state.enabled()) { + ucache_enabled = true; + LOG_INFO("UCache enabled - threshold: %.3f, start: %.2f, end: %.2f, decay: %.2f, relative: %s, reset: %s", + ucache_config.reuse_threshold, + ucache_config.start_percent, + ucache_config.end_percent, + ucache_config.error_decay_rate, + ucache_config.use_relative_threshold ? "true" : "false", + ucache_config.reset_error_on_compute ? "true" : "false"); + } else { + LOG_WARN("UCache requested but could not be initialized for this run"); + } + } + } else if (cache_params->mode == SD_CACHE_DBCACHE || + cache_params->mode == SD_CACHE_TAYLORSEER || + cache_params->mode == SD_CACHE_CACHE_DIT) { + bool cachedit_supported = sd_version_is_dit(version); + if (!cachedit_supported) { + LOG_WARN("CacheDIT requested but not supported for this model type (only DiT models)"); + } else { + DBCacheConfig dbcfg; + dbcfg.enabled = (cache_params->mode == SD_CACHE_DBCACHE || + cache_params->mode == SD_CACHE_CACHE_DIT); + dbcfg.Fn_compute_blocks = cache_params->Fn_compute_blocks; + dbcfg.Bn_compute_blocks = cache_params->Bn_compute_blocks; + dbcfg.residual_diff_threshold = cache_params->residual_diff_threshold; + dbcfg.max_warmup_steps = cache_params->max_warmup_steps; + dbcfg.max_cached_steps = cache_params->max_cached_steps; + dbcfg.max_continuous_cached_steps = cache_params->max_continuous_cached_steps; + if (cache_params->scm_mask != nullptr && strlen(cache_params->scm_mask) > 0) { + dbcfg.steps_computation_mask = parse_scm_mask(cache_params->scm_mask); + } + dbcfg.scm_policy_dynamic = cache_params->scm_policy_dynamic; + + TaylorSeerConfig tcfg; + tcfg.enabled = (cache_params->mode == SD_CACHE_TAYLORSEER || + cache_params->mode == SD_CACHE_CACHE_DIT); + tcfg.n_derivatives = cache_params->taylorseer_n_derivatives; + tcfg.skip_interval_steps = cache_params->taylorseer_skip_interval; + + cachedit_state.init(dbcfg, tcfg); + if (cachedit_state.enabled()) { + cachedit_enabled = true; + LOG_INFO("CacheDIT enabled - mode: %s, Fn: %d, Bn: %d, threshold: %.3f, warmup: %d", + cache_params->mode == SD_CACHE_CACHE_DIT ? "DBCache+TaylorSeer" : (cache_params->mode == SD_CACHE_DBCACHE ? "DBCache" : "TaylorSeer"), + dbcfg.Fn_compute_blocks, + dbcfg.Bn_compute_blocks, + dbcfg.residual_diff_threshold, + dbcfg.max_warmup_steps); + } else { + LOG_WARN("CacheDIT requested but could not be initialized for this run"); + } + } } } + if (ucache_enabled) { + ucache_state.set_sigmas(sigmas); + } + + if (cachedit_enabled) { + cachedit_state.set_sigmas(sigmas); + } + size_t steps = sigmas.size() - 1; struct ggml_tensor* x = ggml_dup_tensor(work_ctx, init_latent); copy_ggml_tensor(x, init_latent); @@ -1696,6 +1782,91 @@ class StableDiffusionGGML { return easycache_step_active && easycache_state.is_step_skipped(); }; + const bool ucache_step_active = ucache_enabled && step > 0; + int ucache_step_index = ucache_step_active ? (step - 1) : -1; + if (ucache_step_active) { + ucache_state.begin_step(ucache_step_index, sigma); + } + + auto ucache_before_condition = [&](const SDCondition* condition, struct ggml_tensor* output_tensor) -> bool { + if (!ucache_step_active || condition == nullptr || output_tensor == nullptr) { + return false; + } + return ucache_state.before_condition(condition, + diffusion_params.x, + output_tensor, + sigma, + ucache_step_index); + }; + + auto ucache_after_condition = [&](const SDCondition* condition, struct ggml_tensor* output_tensor) { + if (!ucache_step_active || condition == nullptr || output_tensor == nullptr) { + return; + } + ucache_state.after_condition(condition, + diffusion_params.x, + output_tensor); + }; + + auto ucache_step_is_skipped = [&]() { + return ucache_step_active && ucache_state.is_step_skipped(); + }; + + const bool cachedit_step_active = cachedit_enabled && step > 0; + int cachedit_step_index = cachedit_step_active ? (step - 1) : -1; + if (cachedit_step_active) { + cachedit_state.begin_step(cachedit_step_index, sigma); + } + + auto cachedit_before_condition = [&](const SDCondition* condition, struct ggml_tensor* output_tensor) -> bool { + if (!cachedit_step_active || condition == nullptr || output_tensor == nullptr) { + return false; + } + return cachedit_state.before_condition(condition, + diffusion_params.x, + output_tensor, + sigma, + cachedit_step_index); + }; + + auto cachedit_after_condition = [&](const SDCondition* condition, struct ggml_tensor* output_tensor) { + if (!cachedit_step_active || condition == nullptr || output_tensor == nullptr) { + return; + } + cachedit_state.after_condition(condition, + diffusion_params.x, + output_tensor); + }; + + auto cachedit_step_is_skipped = [&]() { + return cachedit_step_active && cachedit_state.is_step_skipped(); + }; + + auto cache_before_condition = [&](const SDCondition* condition, struct ggml_tensor* output_tensor) -> bool { + if (easycache_step_active) { + return easycache_before_condition(condition, output_tensor); + } else if (ucache_step_active) { + return ucache_before_condition(condition, output_tensor); + } else if (cachedit_step_active) { + return cachedit_before_condition(condition, output_tensor); + } + return false; + }; + + auto cache_after_condition = [&](const SDCondition* condition, struct ggml_tensor* output_tensor) { + if (easycache_step_active) { + easycache_after_condition(condition, output_tensor); + } else if (ucache_step_active) { + ucache_after_condition(condition, output_tensor); + } else if (cachedit_step_active) { + cachedit_after_condition(condition, output_tensor); + } + }; + + auto cache_step_is_skipped = [&]() { + return easycache_step_is_skipped() || ucache_step_is_skipped() || cachedit_step_is_skipped(); + }; + std::vector scaling = denoiser->get_scalings(sigma); GGML_ASSERT(scaling.size() == 3); float c_skip = scaling[0]; @@ -1771,7 +1942,7 @@ class StableDiffusionGGML { active_condition = &id_cond; } - bool skip_model = easycache_before_condition(active_condition, *active_output); + bool skip_model = cache_before_condition(active_condition, *active_output); if (!skip_model) { if (!work_diffusion_model->compute(n_threads, diffusion_params, @@ -1779,10 +1950,10 @@ class StableDiffusionGGML { LOG_ERROR("diffusion model compute failed"); return nullptr; } - easycache_after_condition(active_condition, *active_output); + cache_after_condition(active_condition, *active_output); } - bool current_step_skipped = easycache_step_is_skipped(); + bool current_step_skipped = cache_step_is_skipped(); float* negative_data = nullptr; if (has_unconditioned) { @@ -1794,12 +1965,12 @@ class StableDiffusionGGML { LOG_ERROR("controlnet compute failed"); } } - current_step_skipped = easycache_step_is_skipped(); + current_step_skipped = cache_step_is_skipped(); diffusion_params.controls = controls; diffusion_params.context = uncond.c_crossattn; diffusion_params.c_concat = uncond.c_concat; diffusion_params.y = uncond.c_vector; - bool skip_uncond = easycache_before_condition(&uncond, out_uncond); + bool skip_uncond = cache_before_condition(&uncond, out_uncond); if (!skip_uncond) { if (!work_diffusion_model->compute(n_threads, diffusion_params, @@ -1807,7 +1978,7 @@ class StableDiffusionGGML { LOG_ERROR("diffusion model compute failed"); return nullptr; } - easycache_after_condition(&uncond, out_uncond); + cache_after_condition(&uncond, out_uncond); } negative_data = (float*)out_uncond->data; } @@ -1817,7 +1988,7 @@ class StableDiffusionGGML { diffusion_params.context = img_cond.c_crossattn; diffusion_params.c_concat = img_cond.c_concat; diffusion_params.y = img_cond.c_vector; - bool skip_img_cond = easycache_before_condition(&img_cond, out_img_cond); + bool skip_img_cond = cache_before_condition(&img_cond, out_img_cond); if (!skip_img_cond) { if (!work_diffusion_model->compute(n_threads, diffusion_params, @@ -1825,7 +1996,7 @@ class StableDiffusionGGML { LOG_ERROR("diffusion model compute failed"); return nullptr; } - easycache_after_condition(&img_cond, out_img_cond); + cache_after_condition(&img_cond, out_img_cond); } img_cond_data = (float*)out_img_cond->data; } @@ -1835,7 +2006,7 @@ class StableDiffusionGGML { float* skip_layer_data = has_skiplayer ? (float*)out_skip->data : nullptr; if (is_skiplayer_step) { LOG_DEBUG("Skipping layers at step %d\n", step); - if (!easycache_step_is_skipped()) { + if (!cache_step_is_skipped()) { // skip layer (same as conditioned) diffusion_params.context = cond.c_crossattn; diffusion_params.c_concat = cond.c_concat; @@ -1939,6 +2110,48 @@ class StableDiffusionGGML { } } + if (ucache_enabled) { + size_t total_steps = sigmas.size() > 0 ? sigmas.size() - 1 : 0; + if (ucache_state.total_steps_skipped > 0 && total_steps > 0) { + if (ucache_state.total_steps_skipped < static_cast(total_steps)) { + double speedup = static_cast(total_steps) / + static_cast(total_steps - ucache_state.total_steps_skipped); + LOG_INFO("UCache skipped %d/%zu steps (%.2fx estimated speedup)", + ucache_state.total_steps_skipped, + total_steps, + speedup); + } else { + LOG_INFO("UCache skipped %d/%zu steps", + ucache_state.total_steps_skipped, + total_steps); + } + } else if (total_steps > 0) { + LOG_INFO("UCache completed without skipping steps"); + } + } + + if (cachedit_enabled) { + size_t total_steps = sigmas.size() > 0 ? sigmas.size() - 1 : 0; + if (cachedit_state.total_steps_skipped > 0 && total_steps > 0) { + if (cachedit_state.total_steps_skipped < static_cast(total_steps)) { + double speedup = static_cast(total_steps) / + static_cast(total_steps - cachedit_state.total_steps_skipped); + LOG_INFO("CacheDIT skipped %d/%zu steps (%.2fx estimated speedup), accum_diff: %.4f", + cachedit_state.total_steps_skipped, + total_steps, + speedup, + cachedit_state.accumulated_residual_diff); + } else { + LOG_INFO("CacheDIT skipped %d/%zu steps, accum_diff: %.4f", + cachedit_state.total_steps_skipped, + total_steps, + cachedit_state.accumulated_residual_diff); + } + } else if (total_steps > 0) { + LOG_INFO("CacheDIT completed without skipping steps"); + } + } + if (inverse_noise_scaling) { x = denoiser->inverse_noise_scaling(sigmas[sigmas.size() - 1], x); } @@ -2554,12 +2767,25 @@ enum lora_apply_mode_t str_to_lora_apply_mode(const char* str) { return LORA_APPLY_MODE_COUNT; } -void sd_easycache_params_init(sd_easycache_params_t* easycache_params) { - *easycache_params = {}; - easycache_params->enabled = false; - easycache_params->reuse_threshold = 0.2f; - easycache_params->start_percent = 0.15f; - easycache_params->end_percent = 0.95f; +void sd_cache_params_init(sd_cache_params_t* cache_params) { + *cache_params = {}; + cache_params->mode = SD_CACHE_DISABLED; + cache_params->reuse_threshold = 1.0f; + cache_params->start_percent = 0.15f; + cache_params->end_percent = 0.95f; + cache_params->error_decay_rate = 1.0f; + cache_params->use_relative_threshold = true; + cache_params->reset_error_on_compute = true; + cache_params->Fn_compute_blocks = 8; + cache_params->Bn_compute_blocks = 0; + cache_params->residual_diff_threshold = 0.08f; + cache_params->max_warmup_steps = 8; + cache_params->max_cached_steps = -1; + cache_params->max_continuous_cached_steps = -1; + cache_params->taylorseer_n_derivatives = 1; + cache_params->taylorseer_skip_interval = 1; + cache_params->scm_mask = nullptr; + cache_params->scm_policy_dynamic = true; } void sd_ctx_params_init(sd_ctx_params_t* sd_ctx_params) { @@ -2724,7 +2950,7 @@ void sd_img_gen_params_init(sd_img_gen_params_t* sd_img_gen_params) { sd_img_gen_params->control_strength = 0.9f; sd_img_gen_params->pm_params = {nullptr, 0, nullptr, 20.f}; sd_img_gen_params->vae_tiling_params = {false, 0, 0, 0.5f, 0.0f, 0.0f}; - sd_easycache_params_init(&sd_img_gen_params->easycache); + sd_cache_params_init(&sd_img_gen_params->cache); } char* sd_img_gen_params_to_str(const sd_img_gen_params_t* sd_img_gen_params) { @@ -2768,12 +2994,18 @@ char* sd_img_gen_params_to_str(const sd_img_gen_params_t* sd_img_gen_params) { sd_img_gen_params->pm_params.id_images_count, SAFE_STR(sd_img_gen_params->pm_params.id_embed_path), BOOL_STR(sd_img_gen_params->vae_tiling_params.enabled)); + const char* cache_mode_str = "disabled"; + if (sd_img_gen_params->cache.mode == SD_CACHE_EASYCACHE) { + cache_mode_str = "easycache"; + } else if (sd_img_gen_params->cache.mode == SD_CACHE_UCACHE) { + cache_mode_str = "ucache"; + } snprintf(buf + strlen(buf), 4096 - strlen(buf), - "easycache: %s (threshold=%.3f, start=%.2f, end=%.2f)\n", - sd_img_gen_params->easycache.enabled ? "enabled" : "disabled", - sd_img_gen_params->easycache.reuse_threshold, - sd_img_gen_params->easycache.start_percent, - sd_img_gen_params->easycache.end_percent); + "cache: %s (threshold=%.3f, start=%.2f, end=%.2f)\n", + cache_mode_str, + sd_img_gen_params->cache.reuse_threshold, + sd_img_gen_params->cache.start_percent, + sd_img_gen_params->cache.end_percent); free(sample_params_str); return buf; } @@ -2790,7 +3022,7 @@ void sd_vid_gen_params_init(sd_vid_gen_params_t* sd_vid_gen_params) { sd_vid_gen_params->video_frames = 6; sd_vid_gen_params->moe_boundary = 0.875f; sd_vid_gen_params->vace_strength = 1.f; - sd_easycache_params_init(&sd_vid_gen_params->easycache); + sd_cache_params_init(&sd_vid_gen_params->cache); } struct sd_ctx_t { @@ -2869,9 +3101,9 @@ sd_image_t* generate_image_internal(sd_ctx_t* sd_ctx, std::vector ref_images, std::vector ref_latents, bool increase_ref_index, - ggml_tensor* concat_latent = nullptr, - ggml_tensor* denoise_mask = nullptr, - const sd_easycache_params_t* easycache_params = nullptr) { + ggml_tensor* concat_latent = nullptr, + ggml_tensor* denoise_mask = nullptr, + const sd_cache_params_t* cache_params = nullptr) { if (seed < 0) { // Generally, when using the provided command line, the seed is always >0. // However, to prevent potential issues if 'stable-diffusion.cpp' is invoked as a library @@ -3160,7 +3392,7 @@ sd_image_t* generate_image_internal(sd_ctx_t* sd_ctx, denoise_mask, nullptr, 1.0f, - easycache_params); + cache_params); int64_t sampling_end = ggml_time_ms(); if (x_0 != nullptr) { // print_ggml_tensor(x_0); @@ -3498,7 +3730,7 @@ sd_image_t* generate_image(sd_ctx_t* sd_ctx, const sd_img_gen_params_t* sd_img_g sd_img_gen_params->increase_ref_index, concat_latent, denoise_mask, - &sd_img_gen_params->easycache); + &sd_img_gen_params->cache); size_t t2 = ggml_time_ms(); @@ -3869,7 +4101,7 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s denoise_mask, vace_context, sd_vid_gen_params->vace_strength, - &sd_vid_gen_params->easycache); + &sd_vid_gen_params->cache); int64_t sampling_end = ggml_time_ms(); LOG_INFO("sampling(high noise) completed, taking %.2fs", (sampling_end - sampling_start) * 1.0f / 1000); @@ -3906,7 +4138,7 @@ SD_API sd_image_t* generate_video(sd_ctx_t* sd_ctx, const sd_vid_gen_params_t* s denoise_mask, vace_context, sd_vid_gen_params->vace_strength, - &sd_vid_gen_params->easycache); + &sd_vid_gen_params->cache); int64_t sampling_end = ggml_time_ms(); LOG_INFO("sampling completed, taking %.2fs", (sampling_end - sampling_start) * 1.0f / 1000); diff --git a/stable-diffusion.h b/stable-diffusion.h index 9bc1fba5b..53db5100b 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -238,12 +238,34 @@ typedef struct { float style_strength; } sd_pm_params_t; // photo maker +enum sd_cache_mode_t { + SD_CACHE_DISABLED = 0, + SD_CACHE_EASYCACHE, + SD_CACHE_UCACHE, + SD_CACHE_DBCACHE, + SD_CACHE_TAYLORSEER, + SD_CACHE_CACHE_DIT, +}; + typedef struct { - bool enabled; + enum sd_cache_mode_t mode; float reuse_threshold; float start_percent; float end_percent; -} sd_easycache_params_t; + float error_decay_rate; + bool use_relative_threshold; + bool reset_error_on_compute; + int Fn_compute_blocks; + int Bn_compute_blocks; + float residual_diff_threshold; + int max_warmup_steps; + int max_cached_steps; + int max_continuous_cached_steps; + int taylorseer_n_derivatives; + int taylorseer_skip_interval; + const char* scm_mask; + bool scm_policy_dynamic; +} sd_cache_params_t; typedef struct { bool is_high_noise; @@ -273,7 +295,7 @@ typedef struct { float control_strength; sd_pm_params_t pm_params; sd_tiling_params_t vae_tiling_params; - sd_easycache_params_t easycache; + sd_cache_params_t cache; } sd_img_gen_params_t; typedef struct { @@ -295,7 +317,7 @@ typedef struct { int64_t seed; int video_frames; float vace_strength; - sd_easycache_params_t easycache; + sd_cache_params_t cache; } sd_vid_gen_params_t; typedef struct sd_ctx_t sd_ctx_t; @@ -325,7 +347,7 @@ SD_API enum preview_t str_to_preview(const char* str); SD_API const char* sd_lora_apply_mode_name(enum lora_apply_mode_t mode); SD_API enum lora_apply_mode_t str_to_lora_apply_mode(const char* str); -SD_API void sd_easycache_params_init(sd_easycache_params_t* easycache_params); +SD_API void sd_cache_params_init(sd_cache_params_t* cache_params); SD_API void sd_ctx_params_init(sd_ctx_params_t* sd_ctx_params); SD_API char* sd_ctx_params_to_str(const sd_ctx_params_t* sd_ctx_params); diff --git a/ucache.hpp b/ucache.hpp new file mode 100644 index 000000000..92e94a16d --- /dev/null +++ b/ucache.hpp @@ -0,0 +1,404 @@ +#ifndef __UCACHE_HPP__ +#define __UCACHE_HPP__ + +#include +#include +#include +#include + +#include "denoiser.hpp" +#include "ggml_extend.hpp" + +struct UCacheConfig { + bool enabled = false; + float reuse_threshold = 1.0f; + float start_percent = 0.15f; + float end_percent = 0.95f; + float error_decay_rate = 1.0f; + bool use_relative_threshold = true; + bool adaptive_threshold = true; + float early_step_multiplier = 0.5f; + float late_step_multiplier = 1.5f; + bool reset_error_on_compute = true; +}; + +struct UCacheCacheEntry { + std::vector diff; +}; + +struct UCacheState { + UCacheConfig config; + Denoiser* denoiser = nullptr; + float start_sigma = std::numeric_limits::max(); + float end_sigma = 0.0f; + bool initialized = false; + bool initial_step = true; + bool skip_current_step = false; + bool step_active = false; + const SDCondition* anchor_condition = nullptr; + std::unordered_map cache_diffs; + std::vector prev_input; + std::vector prev_output; + float output_prev_norm = 0.0f; + bool has_prev_input = false; + bool has_prev_output = false; + bool has_output_prev_norm = false; + bool has_relative_transformation_rate = false; + float relative_transformation_rate = 0.0f; + float cumulative_change_rate = 0.0f; + float last_input_change = 0.0f; + bool has_last_input_change = false; + int total_steps_skipped = 0; + int current_step_index = -1; + int steps_computed_since_active = 0; + float accumulated_error = 0.0f; + float reference_output_norm = 0.0f; + + struct BlockMetrics { + float sum_transformation_rate = 0.0f; + float sum_output_norm = 0.0f; + int sample_count = 0; + float min_change_rate = std::numeric_limits::max(); + float max_change_rate = 0.0f; + + void reset() { + sum_transformation_rate = 0.0f; + sum_output_norm = 0.0f; + sample_count = 0; + min_change_rate = std::numeric_limits::max(); + max_change_rate = 0.0f; + } + + void record(float change_rate, float output_norm) { + if (std::isfinite(change_rate) && change_rate > 0.0f) { + sum_transformation_rate += change_rate; + sum_output_norm += output_norm; + sample_count++; + if (change_rate < min_change_rate) + min_change_rate = change_rate; + if (change_rate > max_change_rate) + max_change_rate = change_rate; + } + } + + float avg_transformation_rate() const { + return (sample_count > 0) ? (sum_transformation_rate / sample_count) : 0.0f; + } + + float avg_output_norm() const { + return (sample_count > 0) ? (sum_output_norm / sample_count) : 0.0f; + } + }; + BlockMetrics block_metrics; + int total_active_steps = 0; + + void reset_runtime() { + initial_step = true; + skip_current_step = false; + step_active = false; + anchor_condition = nullptr; + cache_diffs.clear(); + prev_input.clear(); + prev_output.clear(); + output_prev_norm = 0.0f; + has_prev_input = false; + has_prev_output = false; + has_output_prev_norm = false; + has_relative_transformation_rate = false; + relative_transformation_rate = 0.0f; + cumulative_change_rate = 0.0f; + last_input_change = 0.0f; + has_last_input_change = false; + total_steps_skipped = 0; + current_step_index = -1; + steps_computed_since_active = 0; + accumulated_error = 0.0f; + reference_output_norm = 0.0f; + block_metrics.reset(); + total_active_steps = 0; + } + + void init(const UCacheConfig& cfg, Denoiser* d) { + config = cfg; + denoiser = d; + initialized = cfg.enabled && d != nullptr; + reset_runtime(); + if (initialized) { + start_sigma = percent_to_sigma(config.start_percent); + end_sigma = percent_to_sigma(config.end_percent); + } + } + + void set_sigmas(const std::vector& sigmas) { + if (!initialized || sigmas.size() < 2) { + return; + } + size_t n_steps = sigmas.size() - 1; + + size_t start_step = static_cast(config.start_percent * n_steps); + size_t end_step = static_cast(config.end_percent * n_steps); + + if (start_step >= n_steps) + start_step = n_steps - 1; + if (end_step >= n_steps) + end_step = n_steps - 1; + + start_sigma = sigmas[start_step]; + end_sigma = sigmas[end_step]; + + if (start_sigma < end_sigma) { + std::swap(start_sigma, end_sigma); + } + } + + bool enabled() const { + return initialized && config.enabled; + } + + float percent_to_sigma(float percent) const { + if (!denoiser) { + return 0.0f; + } + if (percent <= 0.0f) { + return std::numeric_limits::max(); + } + if (percent >= 1.0f) { + return 0.0f; + } + float t = (1.0f - percent) * (TIMESTEPS - 1); + return denoiser->t_to_sigma(t); + } + + void begin_step(int step_index, float sigma) { + if (!enabled()) { + return; + } + if (step_index == current_step_index) { + return; + } + current_step_index = step_index; + skip_current_step = false; + has_last_input_change = false; + step_active = false; + + if (sigma > start_sigma) { + return; + } + if (!(sigma > end_sigma)) { + return; + } + step_active = true; + total_active_steps++; + } + + bool step_is_active() const { + return enabled() && step_active; + } + + bool is_step_skipped() const { + return enabled() && step_active && skip_current_step; + } + + float get_adaptive_threshold(int estimated_total_steps = 0) const { + float base_threshold = config.reuse_threshold; + + if (!config.adaptive_threshold) { + return base_threshold; + } + + int effective_total = estimated_total_steps; + if (effective_total <= 0) { + effective_total = std::max(20, steps_computed_since_active * 2); + } + + float progress = (effective_total > 0) ? (static_cast(steps_computed_since_active) / effective_total) : 0.0f; + + float multiplier = 1.0f; + if (progress < 0.2f) { + multiplier = config.early_step_multiplier; + } else if (progress > 0.8f) { + multiplier = config.late_step_multiplier; + } + + return base_threshold * multiplier; + } + + bool has_cache(const SDCondition* cond) const { + auto it = cache_diffs.find(cond); + return it != cache_diffs.end() && !it->second.diff.empty(); + } + + void update_cache(const SDCondition* cond, ggml_tensor* input, ggml_tensor* output) { + UCacheCacheEntry& entry = cache_diffs[cond]; + size_t ne = static_cast(ggml_nelements(output)); + entry.diff.resize(ne); + float* out_data = (float*)output->data; + float* in_data = (float*)input->data; + + for (size_t i = 0; i < ne; ++i) { + entry.diff[i] = out_data[i] - in_data[i]; + } + } + + void apply_cache(const SDCondition* cond, ggml_tensor* input, ggml_tensor* output) { + auto it = cache_diffs.find(cond); + if (it == cache_diffs.end() || it->second.diff.empty()) { + return; + } + + copy_ggml_tensor(output, input); + float* out_data = (float*)output->data; + const std::vector& diff = it->second.diff; + for (size_t i = 0; i < diff.size(); ++i) { + out_data[i] += diff[i]; + } + } + + bool before_condition(const SDCondition* cond, + ggml_tensor* input, + ggml_tensor* output, + float sigma, + int step_index) { + if (!enabled() || step_index < 0) { + return false; + } + if (step_index != current_step_index) { + begin_step(step_index, sigma); + } + if (!step_active) { + return false; + } + + if (initial_step) { + anchor_condition = cond; + initial_step = false; + } + + bool is_anchor = (cond == anchor_condition); + + if (skip_current_step) { + if (has_cache(cond)) { + apply_cache(cond, input, output); + return true; + } + return false; + } + + if (!is_anchor) { + return false; + } + + if (!has_prev_input || !has_prev_output || !has_cache(cond)) { + return false; + } + + size_t ne = static_cast(ggml_nelements(input)); + if (prev_input.size() != ne) { + return false; + } + + float* input_data = (float*)input->data; + last_input_change = 0.0f; + for (size_t i = 0; i < ne; ++i) { + last_input_change += std::fabs(input_data[i] - prev_input[i]); + } + if (ne > 0) { + last_input_change /= static_cast(ne); + } + has_last_input_change = true; + + if (has_output_prev_norm && has_relative_transformation_rate && + last_input_change > 0.0f && output_prev_norm > 0.0f) { + float approx_output_change_rate = (relative_transformation_rate * last_input_change) / output_prev_norm; + accumulated_error = accumulated_error * config.error_decay_rate + approx_output_change_rate; + + float effective_threshold = get_adaptive_threshold(); + if (config.use_relative_threshold && reference_output_norm > 0.0f) { + effective_threshold = effective_threshold * reference_output_norm; + } + + if (accumulated_error < effective_threshold) { + skip_current_step = true; + total_steps_skipped++; + apply_cache(cond, input, output); + return true; + } else if (config.reset_error_on_compute) { + accumulated_error = 0.0f; + } + } + + return false; + } + + void after_condition(const SDCondition* cond, ggml_tensor* input, ggml_tensor* output) { + if (!step_is_active()) { + return; + } + + update_cache(cond, input, output); + + if (cond != anchor_condition) { + return; + } + + size_t ne = static_cast(ggml_nelements(input)); + float* in_data = (float*)input->data; + prev_input.resize(ne); + for (size_t i = 0; i < ne; ++i) { + prev_input[i] = in_data[i]; + } + has_prev_input = true; + + float* out_data = (float*)output->data; + float output_change = 0.0f; + if (has_prev_output && prev_output.size() == ne) { + for (size_t i = 0; i < ne; ++i) { + output_change += std::fabs(out_data[i] - prev_output[i]); + } + if (ne > 0) { + output_change /= static_cast(ne); + } + } + + prev_output.resize(ne); + for (size_t i = 0; i < ne; ++i) { + prev_output[i] = out_data[i]; + } + has_prev_output = true; + + float mean_abs = 0.0f; + for (size_t i = 0; i < ne; ++i) { + mean_abs += std::fabs(out_data[i]); + } + output_prev_norm = (ne > 0) ? (mean_abs / static_cast(ne)) : 0.0f; + has_output_prev_norm = output_prev_norm > 0.0f; + + if (reference_output_norm == 0.0f) { + reference_output_norm = output_prev_norm; + } + + if (has_last_input_change && last_input_change > 0.0f && output_change > 0.0f) { + float rate = output_change / last_input_change; + if (std::isfinite(rate)) { + relative_transformation_rate = rate; + has_relative_transformation_rate = true; + block_metrics.record(rate, output_prev_norm); + } + } + + has_last_input_change = false; + } + + void log_block_metrics() const { + if (block_metrics.sample_count > 0) { + LOG_INFO("UCacheBlockMetrics: samples=%d, avg_rate=%.4f, min=%.4f, max=%.4f, avg_norm=%.4f", + block_metrics.sample_count, + block_metrics.avg_transformation_rate(), + block_metrics.min_change_rate, + block_metrics.max_change_rate, + block_metrics.avg_output_norm()); + } + } +}; + +#endif // __UCACHE_HPP__ From 98916e825686f8952126b2c54e1c4216eb418dc5 Mon Sep 17 00:00:00 2001 From: leejet Date: Mon, 22 Dec 2025 23:58:28 +0800 Subject: [PATCH 38/49] docs: update README.md --- README.md | 1 + examples/cli/README.md | 19 +++++++++++++------ examples/server/README.md | 19 ++++++++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index aa29f8494..3c66be93a 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ If you want to improve performance or reduce VRAM/RAM usage, please refer to [pe - [Using TAESD to faster decoding](./docs/taesd.md) - [Docker](./docs/docker.md) - [Quantization and GGUF](./docs/quantization_and_gguf.md) +- [Inference acceleration via caching](./docs/caching.md) ## Bindings diff --git a/examples/cli/README.md b/examples/cli/README.md index 0617a4637..568f29d04 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -53,6 +53,9 @@ Context Options: --diffusion-fa use flash attention in the diffusion model --diffusion-conv-direct use ggml_conv2d_direct in the diffusion model --vae-conv-direct use ggml_conv2d_direct in the vae model + --circular enable circular padding for convolutions + --circularx enable circular RoPE wrapping on x-axis (width) only + --circulary enable circular RoPE wrapping on y-axis (height) only --chroma-disable-dit-mask disable dit mask for chroma --chroma-enable-t5-mask enable t5 mask for chroma --type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the @@ -94,6 +97,7 @@ Generation Options: --timestep-shift shift timestep for NitroFusion models (default: 0). recommended N for NitroSD-Realism around 250 and 500 for NitroSD-Vibrant --upscale-repeats Run the ESRGAN upscaler this many times (default: 1) + --upscale-tile-size tile size for ESRGAN upscaling (default: 128) --cfg-scale unconditional guidance scale: (default: 7.0) --img-cfg-scale image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale) --guidance distilled guidance scale for models with guidance input (default: 3.5) @@ -121,18 +125,21 @@ Generation Options: tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise) --high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise - --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, kl_optimal, lcm], - default: discrete + --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, + kl_optimal, lcm], default: discrete --sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0"). --skip-layers layers to skip for SLG steps (default: [7,8,9]) --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) -r, --ref-image reference image for Flux Kontext models (can be used multiple times) --cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level) --cache-option named cache params (key=value format, comma-separated): - - easycache/ucache: threshold=,start=,end=,decay=,relative=,reset= - - dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup= - Examples: "threshold=0.25" or "threshold=1.5,reset=0" + - easycache/ucache: + threshold=,start=,end=,decay=,relative=,reset= + - dbcache/taylorseer/cache-dit: + Fn=,Bn=,threshold=,warmup= + Examples: "threshold=0.25" or + "threshold=1.5,reset=0" --cache-preset cache-dit preset: 'slow'/'s', 'medium'/'m', 'fast'/'f', 'ultra'/'u' - --scm-mask SCM steps mask: comma-separated 0/1 (1=compute, 0=can cache) + --scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g., "1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache --scm-policy SCM policy: 'dynamic' (default) or 'static' ``` diff --git a/examples/server/README.md b/examples/server/README.md index ae1049680..89d8560c2 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -45,6 +45,9 @@ Context Options: --diffusion-fa use flash attention in the diffusion model --diffusion-conv-direct use ggml_conv2d_direct in the diffusion model --vae-conv-direct use ggml_conv2d_direct in the vae model + --circular enable circular padding for convolutions + --circularx enable circular RoPE wrapping on x-axis (width) only + --circulary enable circular RoPE wrapping on y-axis (height) only --chroma-disable-dit-mask disable dit mask for chroma --chroma-enable-t5-mask enable t5 mask for chroma --type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the @@ -114,11 +117,21 @@ Default Generation Options: tcd] (default: euler for Flux/SD3/Wan, euler_a otherwise) --high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd] default: euler for Flux/SD3/Wan, euler_a otherwise - --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, kl_optimal, lcm], - default: discrete + --scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple, + kl_optimal, lcm], default: discrete --sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0"). --skip-layers layers to skip for SLG steps (default: [7,8,9]) --high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9]) -r, --ref-image reference image for Flux Kontext models (can be used multiple times) - --easycache enable EasyCache for DiT models with optional "threshold,start_percent,end_percent" (default: 0.2,0.15,0.95) + --cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level) + --cache-option named cache params (key=value format, comma-separated): + - easycache/ucache: + threshold=,start=,end=,decay=,relative=,reset= + - dbcache/taylorseer/cache-dit: + Fn=,Bn=,threshold=,warmup= + Examples: "threshold=0.25" or + "threshold=1.5,reset=0" + --cache-preset cache-dit preset: 'slow'/'s', 'medium'/'m', 'fast'/'f', 'ultra'/'u' + --scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g., "1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache + --scm-policy SCM policy: 'dynamic' (default) or 'static' ``` From 3e812460cffe183150cf9bd55c44e4698fcec10e Mon Sep 17 00:00:00 2001 From: leejet Date: Tue, 23 Dec 2025 21:37:07 +0800 Subject: [PATCH 39/49] fix: correct ggml_pad_ext (#1133) --- ggml_extend.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ggml_extend.hpp b/ggml_extend.hpp index 3849562f0..76889a8b2 100644 --- a/ggml_extend.hpp +++ b/ggml_extend.hpp @@ -1033,7 +1033,7 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_pad(struct ggml_context* ctx, int p3 = 0, bool circular_x = false, bool circular_y = false) { - return ggml_ext_pad_ext(ctx, x, p0, p0, p1, p1, p2, p2, p3, p3, circular_x, circular_y); + return ggml_ext_pad_ext(ctx, x, 0, p0, 0, p1, 0, p2, 0, p3, circular_x, circular_y); } // w: [OC,IC, KH, KW] @@ -1062,7 +1062,7 @@ __STATIC_INLINE__ struct ggml_tensor* ggml_ext_conv_2d(struct ggml_context* ctx, } if ((p0 != 0 || p1 != 0) && (circular_x || circular_y)) { - x = ggml_ext_pad(ctx, x, p0, p1, 0, 0, circular_x, circular_y); + x = ggml_ext_pad_ext(ctx, x, p0, p0, p1, p1, 0, 0, 0, 0, circular_x, circular_y); p0 = 0; p1 = 0; } From 96fcb13fc097a91a876302659d5fce877514cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=98=A5=E4=B9=94?= <83450930+Liyulingyue@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:43:09 +0800 Subject: [PATCH 40/49] feat: add --serve-html-path option to example server (#1123) --- examples/server/README.md | 1 + examples/server/main.cpp | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/server/README.md b/examples/server/README.md index 89d8560c2..591411944 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -6,6 +6,7 @@ usage: ./bin/sd-server [options] Svr Options: -l, --listen-ip server listen ip (default: 127.0.0.1) --listen-port server listen port (default: 1234) + --serve-html-path path to HTML file to serve at root (optional) -v, --verbose print extra info --color colors the logging tags according to level -h, --help show this help message and exit diff --git a/examples/server/main.cpp b/examples/server/main.cpp index 5c951c075..5f5333da5 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -104,6 +104,7 @@ std::string iso_timestamp_now() { struct SDSvrParams { std::string listen_ip = "127.0.0.1"; int listen_port = 1234; + std::string serve_html_path; bool normal_exit = false; bool verbose = false; bool color = false; @@ -115,7 +116,11 @@ struct SDSvrParams { {"-l", "--listen-ip", "server listen ip (default: 127.0.0.1)", - &listen_ip}}; + &listen_ip}, + {"", + "--serve-html-path", + "path to HTML file to serve at root (optional)", + &serve_html_path}}; options.int_options = { {"", @@ -159,6 +164,11 @@ struct SDSvrParams { LOG_ERROR("error: listen_port should be in the range [0, 65535]"); return false; } + + if (!serve_html_path.empty() && !fs::exists(serve_html_path)) { + LOG_ERROR("error: serve_html_path file does not exist: %s", serve_html_path.c_str()); + return false; + } return true; } @@ -167,6 +177,7 @@ struct SDSvrParams { oss << "SDSvrParams {\n" << " listen_ip: " << listen_ip << ",\n" << " listen_port: \"" << listen_port << "\",\n" + << " serve_html_path: \"" << serve_html_path << "\",\n" << "}"; return oss.str(); } @@ -312,7 +323,18 @@ int main(int argc, const char** argv) { // health svr.Get("/", [&](const httplib::Request&, httplib::Response& res) { - res.set_content(R"({"ok":true,"service":"sd-cpp-http"})", "application/json"); + if (!svr_params.serve_html_path.empty()) { + std::ifstream file(svr_params.serve_html_path); + if (file) { + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + res.set_content(content, "text/html"); + } else { + res.status = 500; + res.set_content("Error: Unable to read HTML file", "text/plain"); + } + } else { + res.set_content("Stable Diffusion Server is running", "text/plain"); + } }); // models endpoint (minimal) From 3e6c428c2781ff9694d1a4810abe37de4ab964e8 Mon Sep 17 00:00:00 2001 From: Weiqi Gao Date: Wed, 24 Dec 2025 22:53:17 +0800 Subject: [PATCH 41/49] chore: use Ninja on Windows to speed up build process (#1120) --- .github/workflows/build.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7f78c354e..bfe532bd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,7 +163,7 @@ jobs: - build: "avx512" defines: "-DGGML_NATIVE=OFF -DGGML_AVX512=ON -DGGML_AVX=ON -DGGML_AVX2=ON -DSD_BUILD_SHARED_LIBS=ON" - build: "cuda12" - defines: "-DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_CUDA_ARCHITECTURES='61;70;75;80;86;89;90;100;120'" + defines: "-DSD_CUDA=ON -DSD_BUILD_SHARED_LIBS=ON -DCMAKE_CUDA_ARCHITECTURES='61;70;75;80;86;89;90;100;120' -DCMAKE_CUDA_FLAGS='-Xcudafe \"--diag_suppress=177\" -Xcudafe \"--diag_suppress=550\"'" - build: 'vulkan' defines: "-DSD_VULKAN=ON -DSD_BUILD_SHARED_LIBS=ON" steps: @@ -191,13 +191,17 @@ jobs: Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}" Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin" + - name: Activate MSVC environment + id: msvc_dev_cmd + uses: ilammy/msvc-dev-cmd@v1 + - name: Build id: cmake_build run: | mkdir build cd build - cmake .. ${{ matrix.defines }} - cmake --build . --config Release + cmake .. -DCMAKE_CXX_FLAGS='/bigobj' -G Ninja -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe ${{ matrix.defines }} + cmake --build . - name: Check AVX512F support id: check_avx512f From 3d5fdd7b375f3b7004117d63845efb8646e968a1 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 24 Dec 2025 22:59:23 +0800 Subject: [PATCH 42/49] feat: add support for more underline loras (#1135) --- name_conversion.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/name_conversion.cpp b/name_conversion.cpp index 6a8ae72c0..3ae229b63 100644 --- a/name_conversion.cpp +++ b/name_conversion.cpp @@ -960,6 +960,7 @@ bool is_first_stage_model_name(const std::string& name) { std::string convert_tensor_name(std::string name, SDVersion version) { bool is_lora = false; bool is_lycoris_underline = false; + bool is_underline = false; std::vector lora_prefix_vec = { "lora.lora.", "lora.lora_", @@ -967,12 +968,27 @@ std::string convert_tensor_name(std::string name, SDVersion version) { "lora.lycoris.", "lora.", }; + std::vector underline_lora_prefix_vec = { + "unet_", + "te_", + "te1_", + "te2_", + "te3_", + "vae_", + }; for (const auto& prefix : lora_prefix_vec) { if (starts_with(name, prefix)) { is_lora = true; name = name.substr(prefix.size()); if (contains(prefix, "lycoris_")) { is_lycoris_underline = true; + } else { + for (const auto& underline_lora_prefix : underline_lora_prefix_vec) { + if (starts_with(name, underline_lora_prefix)) { + is_underline = true; + break; + } + } } break; } @@ -1034,7 +1050,7 @@ std::string convert_tensor_name(std::string name, SDVersion version) { // LOG_DEBUG("name %s %d", name.c_str(), version); - if (sd_version_is_unet(version) || sd_version_is_flux(version) || is_lycoris_underline) { + if (sd_version_is_unet(version) || is_underline || is_lycoris_underline) { name = convert_sep_to_dot(name); } } From a0adcfb148c66bc9480521a3beda925b161a6575 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 24 Dec 2025 23:00:08 +0800 Subject: [PATCH 43/49] feat: add support for qwen image edit 2511 (#1096) --- README.md | 4 +- assets/qwen/qwen_image_edit_2511.png | Bin 0 -> 460826 bytes diffusion_model.hpp | 5 +- docs/qwen_image_edit.md | 15 +++- examples/common/common.hpp | 8 ++ flux.hpp | 13 ++-- qwen_image.hpp | 111 +++++++++++++++++++++++---- stable-diffusion.cpp | 3 +- stable-diffusion.h | 1 + 9 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 assets/qwen/qwen_image_edit_2511.png diff --git a/README.md b/README.md index 3c66be93a..bd00761ec 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ API and command-line option may change frequently.*** - [Ovis-Image](./docs/ovis_image.md) - Image Edit Models - [FLUX.1-Kontext-dev](./docs/kontext.md) - - [Qwen Image Edit/Qwen Image Edit 2509](./docs/qwen_image_edit.md) + - [Qwen Image Edit series](./docs/qwen_image_edit.md) - Video Models - [Wan2.1/Wan2.2](./docs/wan.md) - [PhotoMaker](https://github.com/TencentARC/PhotoMaker) support. @@ -132,7 +132,7 @@ If you want to improve performance or reduce VRAM/RAM usage, please refer to [pe - [FLUX.1-Kontext-dev](./docs/kontext.md) - [Chroma](./docs/chroma.md) - [🔥Qwen Image](./docs/qwen_image.md) -- [🔥Qwen Image Edit/Qwen Image Edit 2509](./docs/qwen_image_edit.md) +- [🔥Qwen Image Edit series](./docs/qwen_image_edit.md) - [🔥Wan2.1/Wan2.2](./docs/wan.md) - [🔥Z-Image](./docs/z_image.md) - [Ovis-Image](./docs/ovis_image.md) diff --git a/assets/qwen/qwen_image_edit_2511.png b/assets/qwen/qwen_image_edit_2511.png new file mode 100644 index 0000000000000000000000000000000000000000..18a26daca7a983ffe8ece9ecfe944f3041677930 GIT binary patch literal 460826 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

q7tXyq*3)wA`1nBeo^v5>ri=h^nX3M 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≻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%0DIZV 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

hJ8;5#AkS+c($iA2J0rzFXI zR!xPwbxwFgavMo&NfiD$B_4k^f$OQW^LZQBHA44Nk#kRwZ413MVK4FJuw~1Vrf2^= zykE<^IA79L?s)$;XJRs$!>AgUH!VqT-`VQZD&?C!lq}SzOw5jyKeUXfuex$xNm*|P zJ9cvX-Hoe(44f@1tEsuW(;p8)T=!1q+`AVOw$4_Njw|Sl#ezm$4S4QsDL^}$GX2@j zy3XJM-Dv_G9s_zerTfCT1Hj3hb7m+sQl=5|il0)ls(3k|~n{I6f@-w@2)u-w~wN;T2wi zKv)iQm1~Hddi9!4oQvjZ%M5#l$gvl@Us8lN!b$E^{JUrHIg$>g7jqfAVbb$bDp1e$}RR6~c z)>|3YF6Ph>qjjCw0TSE7q8vSyo-Fo#JLHtPN`kmt>B0{W&N*d)L1p2Y>Z@1pM7?|U z-_<}U@K$6bQ2mB*@D{J8`1DEQTQYN7(?eltxsLAWz(dOJsSI4IH8Mw3H%Gm>{M!}h zDBR@?!4NqdXl`$XZZ1cBdAFgCxEO4BjY41adcK|ww_dsL7_oZM0(s{pBp_=$v}LeW zE|f?J4M1hWD02Sy(eoSy%uN^ljI;C(Vh)QiYd22!@N_(y^1u~JM(vc4*Klox>tX5l z#hfZ~-($FDjC7CB%W}b`psA;%=!$K~HZNxqI^hx>;gQ7s_xof?Hv{_RV4x(+zEch- z7Z`IAzASv`9SwIQS1cj9Cn{^Uqq^&<=vlb$GhkT{hM(_fZyWulqM)qzkmZNhFJGlj zieGNV5Vfi&gzAy(C?-k#(bgZ^^%7}<&fxkl(5JQ4)u?Ja?2k6`R>}m2O5yxPo~tyy z#^iKwZf7I%UHZq(5c64@ySi$fsZ2s0b*R*{DqPH0G%|{TP6_U#ZQqgJb7&u-&zap17oM%mkM|G$ z_Sq;K|0SmWwBW!GJHb3SN`w!N(vbr&$DHnjzqN+R{sm|cwd_~Y_-`A5Kd84t)^$0Z zYka0a0MIl9S1DRQK=mf{RE4(Csw7`KSOrnIj@w*`>JNVV7l^=5yFha~&qxbZqG5n# ztU1&Nzyq3aL+3XJJo9C#&xWA6l5^$fN{(kxLewS1KkJD#@L^g5<;Uv}z3BNv*uBq{ zpWGV{&Hgsyw0M}=aTW}5LeDT4yJEs#p~?LVi{&)jQw_Y;4!EcCuF^(6*6;h<{O|`e zQAQ?bi~{xk<$zX^(wV=1db?2-NyGwF{$`$*@IYThWk&NbcCSu~tqE^HDHI&(S)S62 zbm-)dd;(qXXmM3+;UT}^80^7$xprr!m3tC5(9=hm+rz87+4&=%!SlAp*-xS8yPMkp zt!c8F5r3azf{q@==^gT11v#V_oR&f3@#=b*2VicEru~0VeXm9c?v+Xt9o)u_CpCl- zxM|KNLGKtMw_oU;#mtR*5XBvHvp@&W@$K}_Xr?Uy7EKbTI?qUM0?ytcmJiR*_0lj^bL+>5_G`rCHRwhRJ5SiX83_oM$c8PRV)O*UYeEng6MekF zp-fWA>7b4fab?1B;O^EltUvxDa5_!0BJRxE;0^VMe%-rw;_bPOwaCTcJnZfz=4o8I zd1e!Gu8_CuMvr|@T)$AE%<=ZEyN?p5Ejen&990#kPt<;X;?FNxD9Xfqcz6*8PTJ;= zSs(iQt?hD0Ny33CB-;}Kl`O#gukWr>1Kg`Hj;&Gz(Mcz_4OoTRzDue zK0T_!zvH>Shi77M&1x#HML)TPYNW7~(8@}*S8ujoT#O>9pZNk(N~&(co&g3Dq)VSNmHP^wgH%e>nZ z8+(KC*}GMFk>3<5Q#~bF5iIppxF)3*XkVin=xSr(`XV>5lmB+iqhK}fYp>R@7IQt` zWQG}s754P5DE?)QPOr2^=M~h!Ws4=YNbce1jRn85q!}aepv4MO_Tq2Qa_ln!U9av< z{D)FK_RQuzoF0X=t@!(S=p(A14-~U&CGHU8WuXDC0ykpAVb5SM%^^)=ak&F?7cYbA z!SZc`w_h{(aFFF#0ws;`S-L?k>U|=@K_%p4l885!hc#d3(GmH&b23hM0n6*m@EB+f zQ7WZ9xU3V>Wt)M=ZdvByeWjz79w~Uu?Zee+^eBiq(Ku(md*Feo#Vb;c7o@NT`Fkd&U-@h{`q zSeg`1O`Dk;Rt~_`|>>Wi`1!6w7*t4icX@-U;?`$e&undz+ z2`XgtKZY+aHx?gp394s9iE|P=&s91in+Ws}s2;=dpxTGD$jSL_V6P<9%T0br#Hc13 zR%Ff@xgowbMs!M(W(2;M5M4o4?ing)wW`R8t@Q6luN6C9!6};-_6F{MM0F+7?42hk zG&rUT+;LOkW^`e~M8KF*>+2+R1!w#?%3J}V&ZLCL+EuE+T|-v0hV;D4B~ATD>f$P! zDo-|W*&ah=1$V(RS(1Zv@N^NfS&YY~{F)lc%;#+}LHqEi)`qwyUIL!*6f~sFX$kME z51l{f7!Q%}CnO}qf2@~XmC7$_0m`O`dXldrBj<14-1qa98vhK&-wIVMmAk2pbac8t zNU7!ujeDowFJqT@u3f9(w7PD8%iqn8@m@aOyLRrt4?>#Up~BrSHE43RAVCjasUR0Ttle)NTm;1#fMLwhmH zPt3G5Z0{wsjbPiv=_Kc^auTU<+qCiLsiE zeA^zoa)xouHB;S(-_QxfDcaeh>Z?4DwDQgn>zYcz0aNk;EB%GFv3sGoZ!22d`O&t0 zq)U61zFo0>dJu2ak(bWvXH1JDm7Kj%MDuR8`sTcX>y*wp+G$Dt+ezPROi6RLnLnNg_D)t_SYx#fxLp=5BW86%3G!V8bVxeBYIY4ulw9Cw?7cQV#v8cLqBL*pBi_Vwj_5XR8C_zsA25V4>ysO#|lOfL~Se30f z-3zO(_h3$I;Dq!yGCRHiCG9+Qfq37F(BtSz&p3DYg9>r%mEdmK^uOV@HlD^$CFv&| zqM&jv?3#?oa+7{_SAKd71qR^l5P((#-rK}l6W~K?nZ=I6cHa=UATKG| zY9Q;h!ybE+&$oy{Cme*x z9A2|@N0pVGzK)TF=c&TCDk0s_)s;xe8Q6uvFK^D{-vOB&p_)=SgCe|64sir(OSgZG z0?Tj8_9ODHhv#~&_=R`_$B0X(a;<)+i4V;3z4`tHZ_Kb5GnGYa(%)BOlU>xC?wd-H z?HgbX4Tu)|TGT@rd2wI1AJ8!e86denWTs zaQaq}Gob0TWasV}nVUxUcYh=V7nc@|e}D1t&W~&AeH~mEGG6A?ak09 zPi3^n$oCZtmO$@^GSxlusA5}kxU#J+-(lrRpDHdz&+JLj<$x!lDMVTPjV0V6p4=nytl^t z?M_*s@=L0<1$!A5w3W?Vde-10Ch(O$#c{R+O@QN9~(_^M$8Rg5P&yd+P9Xuav`Uc`Su> z-Q;i6Le6H4Xh%YoIdf7EG4mjHU>JQgcj3Yl^kOJ-+zKmRj5Rf)Af5ezVc#ky*LfdN ztFtCKJtrPh!)_X@#3u8U66D4C4D>fC6e2vXP_Y$11L`%iiT3mIuJPq(=-gx-Q5HVf z^_ZcqW}l}<3HEYwSGED8GsJ;W+$Q;2ies70HefQR1j4)=p}oLmYW&^T?$J-sE>-qc zg1e)3!fFynN{5Hqxel1pWAtl3?`{E7)L&qnlv;~iW3a%V=Kh_U$EEL7X-h3xfBigKw=n~jli4C9RoI)nQ z{_15^HvWG!5WIXyc~y`cq1qT)93dW~QTZ3Y@{g0n91qi5wyZ)Mqo~5(G|mMnt?K~) ziBivrt%Q*bV!gO|Zo2*Yb@V40TVx^bQp5JDqC`LRhz93TTbiHT9C_z=d?|jd_sYSh z1#)*KKB(#X3kqkxH~WgKccLHkn2)H~X`ueFGrF~sqG1V_6B2-#07ztRB{&G`fxEVC zr}Z2C9e;=K?L~ZQIMDH;c}+;LD*n9sR+m=r@ILRgvlRKWE9i#IZbyL$fl9`nzWLG3 zp#5=_`pHQ@bQ~%?^^k zSn84BpcDh&{6k$UXFKH~yfjNQt3@W$a-wtAatqM3*dL^ z`WxPm&?bS&2$9U$rT)}}X{Jv(#*}v!|BE-ojI9ONpt4kgLhI_B+(*d^I&~HD34Xew zyZj|hQmk!>xSXIbh4+I3K0!gHt=;eFu^fPW0$F^rs;acC0pjTEXe9&6woPAW4TV4`7MA4CS&8-SF_DU&M{Ho@l^Pqt zqQl8jb+MKXb{W5t^c_8{#02LJ;$h<;D|y@D7c$P5%js_qk~Vd85>~ok^wl96>Sf7v zR*MbK7+}4W53ihJBtw93H!*I#gfj3S!C6zmz6p4LX-*q&_5OAMSF(j*ej;h)H2ilh zLUg|5P22qy9yNmhRw70_(TO#@uRG9auYxmMv6=2(oVWXqH>ImGBqxbzWZshNSA9u80pmmT+$3Z(@`Y@Ou^R0#TR2L4pqrIuy<*>K5Bc) z`T4|q1)PFCztyp^HvP6kY`Hni8HEJJ#CtJa<;y8$&1=_gxOMw3)U{gIIUz_p!43I{ z`=5C(vdrpZQgquYR);=|LXk+|?#+PPGs1hnF)2e(I6KjMU&S{c^eyhJz6q~L=Y8F% zVOa)`2!VlEIUVy^$%L=>`l%ZmxP^f;*uFSvv&)t}<@m-0Jl7#gb+K*|STAIszMC_i zjgR{qJd4_?t==;5rA1lATNIas90hGoDXnzdx013CFw9{Y`HTbqIPJp)9#N2suZz{6#mo zt8>W^QEd{9X_xXi8T75rTgOm92`LihQ3a=T9rBt1`5~!wZH6vYwo_{D04pO}1UWXR+qbRvFJXUP7C>+enh~^J#Iva) zh0h**-E#AS&qh9?w~MeL*CENXv^3V5nbAh3S&3wUSmQ?AA{3Lk%};39&c&^UmGPAx zcIjjea$}tLf`W}Hpt^_nx#YeOA%s!xq+g#|@~fG{|z9zAfozJ3;6?1tV}E`7qI6iYI?mq8=Y{=Em3 zR8<@HPW(*^lK-nwS0$6MR3`SBxVN^lHvI9y(Ggv~1u7q_(N#|F+L7)1Y~XRy=!;*y ze2N{Kl+HXV7o}At2g}d$BEh=d|9{!{Y5)H+bzL^}_P92h^5jI<2~_15lDKZ1GvSsz z3G`7`c6WjbhX;fQ<;#5aiMA?2eKuh_jJGBg|G#wpY$a3|6Q8?VyE1=`m9p;sl%^F# zU*xCDJDr4Ywgr~E68m5N{PA`ATifg2+D|~zarA)>=Vg;j#Umhkk4V!^MdgAfnDsAh zGF^4UR%>fqjp*9DKKP5P%5PfA6%v_f*}?uWtnR6FGTYBAARim zSS>5qr}#yY6srM70kg`7ywu?J9msBLc+tb={^!8@W|@r?1yaZ!(luY^A&Tvz+$w9V z0$LC7pG3g2v#zR}*s#_)2ApC!J;)K0|?|L}k>HD~F# z2#zUkFkzW#hfK*_Q(`VECC0r+&Z;qep_1KT?KDAeJ1NWv)G;M2Ghy9d#kxqgJxdGE zCm!1$w1$*#!0{w+JfBx^4e}BF?vKDk$w5qZte-XKXSTyW`Y%R{l!ZR3>5k~wA=YgVXojFLj8ERhgcrtzu8l!v)lHS^6>TR z%E}ke{|Jal?$O+1!^6Yv?XBP%*ujA}?Fn{hi5yHxBtP$GROj438r(W^2p!qH;4f{m zNE9z}9vb0QzzlR`fi3V{KXrdEf@Qznj>( zqq6}ux=74o!AkMrU_2YN*h|X4Y@NQbB%MmAzP)`L_-PAelE$$EO=x1l+O_O?aHH0> zoj+H%UDxwS5dXNJ|Nk!41kmqGjn9L#OB1O6@nMe%t_KvHilNR?l&*#O$YPmGtu2`v zF*bJ0!#hxy2bxXtO#WFZHRtDl?68bxdVLJF=M|~)HT371E$_9u>2!wnIsP6${!w3! zA^A-X+T4JwSK`zV`fA7rtdRwSD`{vuom;Ocj@!z9N7k8x<67VfzP#%vhVY!0@E*BJ zo*P8A?nms*_;x>$ulLxOk#C=M?Z7Z78lF&vz@Me3_RBg`jpVjB?KWb>;RWHVF z2aDy**g~eG)XsButiUr@DRjsh+o_D3mK}?X?SeH!S3P;3n>XYp=k*mQ*S^w-W)>lN zL&rH@n`Rh?Tr-qb(i#LYOpD)i#Vl)BD3dMB14F5{Of=0eeMKpGJ;m(zX9Z@Fgh~}D zmx3dqLK@o*Ac?;i`;dH8Gw7NIgNu#oENAk+MEYdHO~xzANK43DPY}-m;i}*Pk6>Dr z7&=E%kBOR*N;O{F?+Hl``KEqkJR2Lb%d+pkC>oHsiaAOlX+1M!hco^HlE}#tX z$#R|+v)zc(3%G8&{AX`1qd%J*7mFy@JpBhEgsMKH=4m;Dg z1M;wgmH0x)>#KpRsTN$PzB!k7MkQnhS}$4nN8?_Atvdwiap%9V!9Az9s;jB3D=PVK z!&-u~o)98y0>{SXjE%3L#k!m|iv%*H8gUf7h2Kh*iwSCG)E!Xa-gV&qrPR$MvJEN+W1fA#66yY`$Ay`B-`VV%B8z!_t3wUkOr9w3QH6Jq#|inweVpSb~3$k}4NFZ5); zY)%fmdOtQ|g87f8iSIUHA#k)eep_7EWW7@p{!UM@7gUCa=k z*6JG|*FaFkb+S$x+433jFzB?2Mm>m67Z)Zr2AQb|-~DQ_5?_9ePOag;{(&6i`7D!U zeDIZ`aCoQa?)HLxaKAfzyf5AAF?pFHYm1NA)YQqq2Bs`jdPoorGduX|_Nn94O4n4m zh8@ZwonRm}Yd4!fYW3hOq26pmEF!RhZEM%A#k35t)Vl3~mCK#$T~q$5a@QJiQ}vvT z#%O(2q$5A>?%D`LFwaGuR~;kXO_cCWH)y*K^(VtwUsm zCx*U6V+hWysaB|Bk;BCbONS&+eE#E|motx-W{f^0AH;VB4=XgxJQMXTGPWG~oPmS2 zT^Hu0-I7OhTef`Uy@oZc!T=u)(H?Axm7fwV)Ksp1E=#GC9Z7nSr)2w(vXr5(w&+r} z{0ZLzOZHpvgw>XYgzznEnxD682~%acQywa@!+8C$L52{mJ%lTi93;z*O&YYUb|{;y~p${RdV(t{gJmucd83)je~AMJH6mUF0P4# zvQtjzden2u8zA z(Nv#4HA0S3$TyVthGZmE>!c1ml-o_TVUe59Z)?xPPN(w~!I(sBO6MkDw;1jDM@#nZ zj1cQ?WlC?uS5H6qhv#d!;Ot-8VVc$%FhCnfdGfTK>?$7|LUnpbX@GwJn65=HC-JXA zm_>UU>$NpF={@02BIm%5mQbrg_658j`go)G5NmxQo#nES<8uK&)DhU<8X}f!2 z?K{}Tea!ojV6G;%PI1|uZS2!YquwLJskUS_B)6;{ii$(keU6-NPTT=c^i=8Z1aGk4)LIW1~r zdJbTw79%=-2d#p2hub;oIas8!g#!>LB*kB+Axxy4T zozFRRT8h0`lZGZ9wefr?^+z-Kqn{pH>7(QZzGjHqJZ{Q4E}``o0)Nc6L8HAYN|M<> z9r=|WNbl{3!Uqxc$wbXd{@BUAd>%jJQt6qvut!RoIx}DAHe&9PB8*}x&nuIwC?Xvw zMg9=k7BagnfvXphjMXM5bcq2o;~ZlJ)jc&I1Ytg>LBP%lC4OQ zUd; zCwkMG6`ZXTj3^Qe+ZV9*J^`#&6_ECz&s6Ia(@P%bFY)m*$T3Zo8Xfyj zNT;?g)-w9PxTok=Rk}X0^la06eQ(p<0-2{7mzPo#4b8J|;V|{<;O@mjnV3vKFl_*_ zjW5(?1p`68r=pLu923hd(B+3N4~5ZdTpY`VNC#K4}UzqK_->`})gPtWU5 zS=HHYi$XT|=80U19c;eC+q9tys;Iz`v*Rfv#ccs^{A3)aN)6=arJU{Q?G!(S%zjmz zZ6|z3+!+M43>v)t4}ArP^ZijD@Xb`b*H09r9{!W`~Sfy0Ap z;&&4JM-R@;E{;=h2=XBG1xx->g#9vg10*MbWFcFaSyp%9%itXo-j8!E^=GivHFCwA z3?3NYSlUz~kisADQHBZg@aP-d>q8U0Mpg@E`2rp2pg5#wBRK4z}R>(5wArakggzg_rqoZxgb@>>h z?ngVYYd`*J{c)Gv&nFTo?I*tAD#13%k;IbIX$4ak_#bqcmzQ%TvUS&IUi?ao*`SU? zX27n^z{ZgbXE3d4^Ew=;JZmpS?@Qpo>Hmwc|7J9%{3Dg|A)l)2*R?pgfN9e_agQRn z9&Kcu*5b6(=H9Tw#lVk-_-~rACBgjXQq1mKQoB; z-OaEOdXjJUeoaE(j`Xtv&yWGu6h0=?%{&Bp!Dav}#QT{HWj%g4 zzoahzH7$p5VO><_1KELlN2;=Cl{XysylbQX8}@bV8Osw#GVJ3N{^U3L$7E(%UxHwV zzsN^lOv%!7{s~%MWfkrQJ4BVDn3&g&pV%I{@bhz(5ZsQ;x ze9oFnI)r4u+7GnYv97)r2%u3Oq<#_ox<8^ZnWB?LE*@WXOfEcJ58W1yBpzzy&Lii_ z&s5U5wu-SDsw%Nvr|GT@z_B17@q1ZOu!%}Sm5Q?kymQ1R!>5zA#<-x7_fAE~X4vaJ z>3s66o2D}+o&EP3KaDe2M>i#&%$OzUpsQG#!u^^~pC~#CP%(m@)M5LjYv@YAyW2OF zHr)tjT{Q=Amg3fJaCoeO&i1crii81I$^XqdXIu^&7vp3UxO({WS5omKbc85Slf4<# z#$pI_(v=R zk7RcX;1ojmu?d>1ri@Mxx-a`%v}=PHY!V_C)a(x%PYl8gxt z5k;R+aTa*71-`M9)27h5i67mR#xfb;8RG;}VmN`^Z%XbDka0cy*>ULDk4CJB)TM{* zQfRs{MNgZb0rM2~WvRzn^7YQ(-{yy=^|;=Kf*Rt;H|SnFl3R}4e*h-(WaTKbvXnNz zj<5$%16{VS5U$w!Tt+Vh9_@%scQ|A4kJvW@j6AA8^wr069V9LwVLXpce{4)!BybfJ zkuA2)&e6gGGZadFz9qBo%i2F_>*Pv)FUNT5?+!gc=MRiZKAM$S!W*AnyS8)SQBt;2 z0>!A$Ni=Mw9@Lw69A7j7w9aQGT%#BC!(Hd`x((&*#%%My#J6&M=ed^Y!m9zHAt5_= z#BO=8kKS_#ms&h3U0dvDYhx&1iZh-uv2DIm_!DHYLA2r}0WVlBPb}lDwHzstUK-77Af*A5L{ zl+eP-#DWogUuclJJbka{Obajw3#~aA#HrF6=55%3L;_4Hyel30=lYp5XRy%P9OWei z=cI?PU<@cDr6;yad&BQXzL3w1TT6)EqPZm@A)uEKnwx903!hUAUn)|k$#k^T8Z;sU z0&;R8vHYOS3X+oGTNFP;nckW$_Bo^UdaEJX-x4WqDpDf#m=|ztKlBL4a|4-Oa@r9w zYhfBf>>X7hfkYk8D#2u5o2&FZ_Kzn^T+xHw=s~9}F%XAK*OPT{B%gPn(-9JS9C~q= zFmB@k9zZ)yI3?x&y6ApvOg5c+6wS3oo_8SWUQURAoKAzgX{k1?z~!UF^@zOZ^DS3< zui1+B%}Gge)i0Ac!%g54Z{eEl9Mkd9KM7L$GV!(ibL8^>pD*|@&lXa#J^O~HLwd3f zPYxS_s+D8LTu*0i{-?;G2WoHMqndOkbJW147pQz7on%OOQkG;4;Nms-yYx3;KIH}S zaa}_!Asbnx#L$htvR-$#m-ORXvDpT%K9K_7bU1k^0o=0&So;##rrw?nHXFmss3r$O zk$OWpK5*|`EMadpw1y;nelaJC?ssa6mTAtv%ivPVVcT^># zBX&m>suu{r3HX*=SU-ya<|_8dE&I<0>*#PeBG;niMe-_vn9^>t#Y4vd=vqXL@BCgL z_{GD+Zc7D~YV0eCZxHCn6xrEBp0L(ZS%WGDdiFT>;$2>e=$dPg8y

<&_vY>j~6A8FI3#~@QFOP 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 z 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

KDAr{(30 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 G`u_uATpN%8 literal 0 HcmV?d00001 diff --git a/diffusion_model.hpp b/diffusion_model.hpp index c4e0ba1d0..06cbecc28 100644 --- a/diffusion_model.hpp +++ b/diffusion_model.hpp @@ -320,8 +320,9 @@ struct QwenImageModel : public DiffusionModel { bool offload_params_to_cpu, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "model.diffusion_model", - SDVersion version = VERSION_QWEN_IMAGE) - : prefix(prefix), qwen_image(backend, offload_params_to_cpu, tensor_storage_map, prefix, version) { + SDVersion version = VERSION_QWEN_IMAGE, + bool zero_cond_t = false) + : prefix(prefix), qwen_image(backend, offload_params_to_cpu, tensor_storage_map, prefix, version, zero_cond_t) { } std::string get_desc() override { diff --git a/docs/qwen_image_edit.md b/docs/qwen_image_edit.md index d376a2830..4a8b01728 100644 --- a/docs/qwen_image_edit.md +++ b/docs/qwen_image_edit.md @@ -9,6 +9,9 @@ - Qwen Image Edit 2509 - safetensors: https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/tree/main/split_files/diffusion_models - gguf: https://huggingface.co/QuantStack/Qwen-Image-Edit-2509-GGUF/tree/main + - Qwen Image Edit 2511 + - safetensors: https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/tree/main/split_files/diffusion_models + - gguf: https://huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF/tree/main - Download vae - safetensors: https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/tree/main/split_files/vae - Download qwen_2.5_vl 7b @@ -32,4 +35,14 @@ .\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\Qwen-Image-Edit-2509-Q4_K_S.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct-Q8_0.gguf --llm_vision ..\..\ComfyUI\models\text_encoders\Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'Qwen Image Edit 2509'" ``` -qwen_image_edit_2509 \ No newline at end of file +qwen_image_edit_2509 + +### Qwen Image Edit 2511 + +To use the new Qwen Image Edit 2511 mode, the `--qwen-image-zero-cond-t` flag must be enabled; otherwise, image editing quality will degrade significantly. + +``` +.\bin\Release\sd-cli.exe --diffusion-model ..\..\ComfyUI\models\diffusion_models\qwen-image-edit-2511-Q4_K_M.gguf --vae ..\..\ComfyUI\models\vae\qwen_image_vae.safetensors --llm ..\..\ComfyUI\models\text_encoders\qwen_2.5_vl_7b.safetensors --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ..\assets\flux\flux1-dev-q8_0.png -p "change 'flux.cpp' to 'edit.cpp'" --qwen-image-zero-cond-t +``` + +qwen_image_edit_2509 \ No newline at end of file diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 868b06e92..74560814e 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -457,6 +457,8 @@ struct SDContextParams { bool chroma_use_t5_mask = false; int chroma_t5_mask_pad = 1; + bool qwen_image_zero_cond_t = false; + prediction_t prediction = PREDICTION_COUNT; lora_apply_mode_t lora_apply_mode = LORA_APPLY_AUTO; @@ -625,6 +627,10 @@ struct SDContextParams { "--chroma-disable-dit-mask", "disable dit mask for chroma", false, &chroma_use_dit_mask}, + {"", + "--qwen-image-zero-cond-t", + "enable zero_cond_t for qwen image", + true, &qwen_image_zero_cond_t}, {"", "--chroma-enable-t5-mask", "enable t5 mask for chroma", @@ -888,6 +894,7 @@ struct SDContextParams { << " circular_x: " << (circular_x ? "true" : "false") << ",\n" << " circular_y: " << (circular_y ? "true" : "false") << ",\n" << " chroma_use_dit_mask: " << (chroma_use_dit_mask ? "true" : "false") << ",\n" + << " qwen_image_zero_cond_t: " << (qwen_image_zero_cond_t ? "true" : "false") << ",\n" << " chroma_use_t5_mask: " << (chroma_use_t5_mask ? "true" : "false") << ",\n" << " chroma_t5_mask_pad: " << chroma_t5_mask_pad << ",\n" << " prediction: " << sd_prediction_name(prediction) << ",\n" @@ -953,6 +960,7 @@ struct SDContextParams { chroma_use_dit_mask, chroma_use_t5_mask, chroma_t5_mask_pad, + qwen_image_zero_cond_t, flow_shift, }; return sd_ctx_params; diff --git a/flux.hpp b/flux.hpp index bcff04cfa..86e2007ad 100644 --- a/flux.hpp +++ b/flux.hpp @@ -233,14 +233,17 @@ namespace Flux { __STATIC_INLINE__ struct ggml_tensor* modulate(struct ggml_context* ctx, struct ggml_tensor* x, struct ggml_tensor* shift, - struct ggml_tensor* scale) { + struct ggml_tensor* scale, + bool skip_reshape = false) { // x: [N, L, C] // scale: [N, C] // shift: [N, C] - scale = ggml_reshape_3d(ctx, scale, scale->ne[0], 1, scale->ne[1]); // [N, 1, C] - shift = ggml_reshape_3d(ctx, shift, shift->ne[0], 1, shift->ne[1]); // [N, 1, C] - x = ggml_add(ctx, x, ggml_mul(ctx, x, scale)); - x = ggml_add(ctx, x, shift); + if (!skip_reshape) { + scale = ggml_reshape_3d(ctx, scale, scale->ne[0], 1, scale->ne[1]); // [N, 1, C] + shift = ggml_reshape_3d(ctx, shift, shift->ne[0], 1, shift->ne[1]); // [N, 1, C] + } + x = ggml_add(ctx, x, ggml_mul(ctx, x, scale)); + x = ggml_add(ctx, x, shift); return x; } diff --git a/qwen_image.hpp b/qwen_image.hpp index ed1c98308..4b29d63d3 100644 --- a/qwen_image.hpp +++ b/qwen_image.hpp @@ -191,11 +191,16 @@ namespace Qwen { }; class QwenImageTransformerBlock : public GGMLBlock { + protected: + bool zero_cond_t; + public: QwenImageTransformerBlock(int64_t dim, int64_t num_attention_heads, int64_t attention_head_dim, - float eps = 1e-6) { + float eps = 1e-6, + bool zero_cond_t = false) + : zero_cond_t(zero_cond_t) { // img_mod.0 is nn.SiLU() blocks["img_mod.1"] = std::shared_ptr(new Linear(dim, 6 * dim, true)); @@ -220,11 +225,37 @@ namespace Qwen { eps)); } + std::vector get_mod_params_vec(ggml_context* ctx, ggml_tensor* mod_params, ggml_tensor* index = nullptr) { + // index: [N, n_img_token] + // mod_params: [N, hidden_size * 12] + if (index == nullptr) { + return ggml_ext_chunk(ctx, mod_params, 6, 0); + } + mod_params = ggml_reshape_1d(ctx, mod_params, ggml_nelements(mod_params)); + auto mod_params_vec = ggml_ext_chunk(ctx, mod_params, 12, 0); + index = ggml_reshape_3d(ctx, index, 1, index->ne[0], index->ne[1]); // [N, n_img_token, 1] + index = ggml_repeat_4d(ctx, index, mod_params_vec[0]->ne[0], index->ne[1], index->ne[2], index->ne[3]); // [N, n_img_token, hidden_size] + std::vector mod_results; + for (int i = 0; i < 6; i++) { + auto mod_0 = mod_params_vec[i]; + auto mod_1 = mod_params_vec[i + 6]; + + // mod_result = torch.where(index == 0, mod_0, mod_1) + // mod_result = (1 - index)*mod_0 + index*mod_1 + mod_0 = ggml_sub(ctx, ggml_repeat(ctx, mod_0, index), ggml_mul(ctx, index, mod_0)); // [N, n_img_token, hidden_size] + mod_1 = ggml_mul(ctx, index, mod_1); // [N, n_img_token, hidden_size] + auto mod_result = ggml_add(ctx, mod_0, mod_1); + mod_results.push_back(mod_result); + } + return mod_results; + } + virtual std::pair forward(GGMLRunnerContext* ctx, struct ggml_tensor* img, struct ggml_tensor* txt, struct ggml_tensor* t_emb, - struct ggml_tensor* pe) { + struct ggml_tensor* pe, + struct ggml_tensor* modulate_index = nullptr) { // img: [N, n_img_token, hidden_size] // txt: [N, n_txt_token, hidden_size] // pe: [n_img_token + n_txt_token, d_head/2, 2, 2] @@ -244,14 +275,18 @@ namespace Qwen { auto img_mod_params = ggml_silu(ctx->ggml_ctx, t_emb); img_mod_params = img_mod_1->forward(ctx, img_mod_params); - auto img_mod_param_vec = ggml_ext_chunk(ctx->ggml_ctx, img_mod_params, 6, 0); + auto img_mod_param_vec = get_mod_params_vec(ctx->ggml_ctx, img_mod_params, modulate_index); + + if (zero_cond_t) { + t_emb = ggml_ext_chunk(ctx->ggml_ctx, t_emb, 2, 1)[0]; + } auto txt_mod_params = ggml_silu(ctx->ggml_ctx, t_emb); txt_mod_params = txt_mod_1->forward(ctx, txt_mod_params); - auto txt_mod_param_vec = ggml_ext_chunk(ctx->ggml_ctx, txt_mod_params, 6, 0); + auto txt_mod_param_vec = get_mod_params_vec(ctx->ggml_ctx, txt_mod_params); auto img_normed = img_norm1->forward(ctx, img); - auto img_modulated = Flux::modulate(ctx->ggml_ctx, img_normed, img_mod_param_vec[0], img_mod_param_vec[1]); + auto img_modulated = Flux::modulate(ctx->ggml_ctx, img_normed, img_mod_param_vec[0], img_mod_param_vec[1], modulate_index != nullptr); auto img_gate1 = img_mod_param_vec[2]; auto txt_normed = txt_norm1->forward(ctx, txt); @@ -264,7 +299,7 @@ namespace Qwen { txt = ggml_add(ctx->ggml_ctx, txt, ggml_mul(ctx->ggml_ctx, txt_attn_output, txt_gate1)); auto img_normed2 = img_norm2->forward(ctx, img); - auto img_modulated2 = Flux::modulate(ctx->ggml_ctx, img_normed2, img_mod_param_vec[3], img_mod_param_vec[4]); + auto img_modulated2 = Flux::modulate(ctx->ggml_ctx, img_normed2, img_mod_param_vec[3], img_mod_param_vec[4], modulate_index != nullptr); auto img_gate2 = img_mod_param_vec[5]; auto txt_normed2 = txt_norm2->forward(ctx, txt); @@ -325,6 +360,7 @@ namespace Qwen { float theta = 10000; std::vector axes_dim = {16, 56, 56}; int64_t axes_dim_sum = 128; + bool zero_cond_t = false; }; class QwenImageModel : public GGMLBlock { @@ -346,7 +382,8 @@ namespace Qwen { auto block = std::shared_ptr(new QwenImageTransformerBlock(inner_dim, params.num_attention_heads, params.attention_head_dim, - 1e-6f)); + 1e-6f, + params.zero_cond_t)); blocks["transformer_blocks." + std::to_string(i)] = block; } @@ -421,7 +458,8 @@ namespace Qwen { struct ggml_tensor* x, struct ggml_tensor* timestep, struct ggml_tensor* context, - struct ggml_tensor* pe) { + struct ggml_tensor* pe, + struct ggml_tensor* modulate_index = nullptr) { auto time_text_embed = std::dynamic_pointer_cast(blocks["time_text_embed"]); auto txt_norm = std::dynamic_pointer_cast(blocks["txt_norm"]); auto img_in = std::dynamic_pointer_cast(blocks["img_in"]); @@ -430,18 +468,26 @@ namespace Qwen { auto proj_out = std::dynamic_pointer_cast(blocks["proj_out"]); auto t_emb = time_text_embed->forward(ctx, timestep); - auto img = img_in->forward(ctx, x); - auto txt = txt_norm->forward(ctx, context); - txt = txt_in->forward(ctx, txt); + if (params.zero_cond_t) { + auto t_emb_0 = time_text_embed->forward(ctx, ggml_ext_zeros(ctx->ggml_ctx, timestep->ne[0], timestep->ne[1], timestep->ne[2], timestep->ne[3])); + t_emb = ggml_concat(ctx->ggml_ctx, t_emb, t_emb_0, 1); + } + auto img = img_in->forward(ctx, x); + auto txt = txt_norm->forward(ctx, context); + txt = txt_in->forward(ctx, txt); for (int i = 0; i < params.num_layers; i++) { auto block = std::dynamic_pointer_cast(blocks["transformer_blocks." + std::to_string(i)]); - auto result = block->forward(ctx, img, txt, t_emb, pe); + auto result = block->forward(ctx, img, txt, t_emb, pe, modulate_index); img = result.first; txt = result.second; } + if (params.zero_cond_t) { + t_emb = ggml_ext_chunk(ctx->ggml_ctx, t_emb, 2, 1)[0]; + } + img = norm_out->forward(ctx, img, t_emb); img = proj_out->forward(ctx, img); @@ -453,7 +499,8 @@ namespace Qwen { struct ggml_tensor* timestep, struct ggml_tensor* context, struct ggml_tensor* pe, - std::vector ref_latents = {}) { + std::vector ref_latents = {}, + struct ggml_tensor* modulate_index = nullptr) { // Forward pass of DiT. // x: [N, C, H, W] // timestep: [N,] @@ -479,7 +526,7 @@ namespace Qwen { int64_t h_len = ((H + (params.patch_size / 2)) / params.patch_size); int64_t w_len = ((W + (params.patch_size / 2)) / params.patch_size); - auto out = forward_orig(ctx, img, timestep, context, pe); // [N, h_len*w_len, ph*pw*C] + auto out = forward_orig(ctx, img, timestep, context, pe, modulate_index); // [N, h_len*w_len, ph*pw*C] if (out->ne[1] > img_tokens) { out = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, out, 0, 2, 1, 3)); // [num_tokens, N, C * patch_size * patch_size] @@ -502,15 +549,19 @@ namespace Qwen { QwenImageParams qwen_image_params; QwenImageModel qwen_image; std::vector pe_vec; + std::vector modulate_index_vec; SDVersion version; QwenImageRunner(ggml_backend_t backend, bool offload_params_to_cpu, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "", - SDVersion version = VERSION_QWEN_IMAGE) + SDVersion version = VERSION_QWEN_IMAGE, + bool zero_cond_t = false) : GGMLRunner(backend, offload_params_to_cpu) { - qwen_image_params.num_layers = 0; + qwen_image_params.num_layers = 0; + qwen_image_params.zero_cond_t = zero_cond_t; + LOG_DEBUG("zero_cond_t: %d", zero_cond_t); for (auto pair : tensor_storage_map) { std::string tensor_name = pair.first; if (tensor_name.find(prefix) == std::string::npos) @@ -576,6 +627,31 @@ namespace Qwen { // pe->data = nullptr; set_backend_tensor_data(pe, pe_vec.data()); + ggml_tensor* modulate_index = nullptr; + if (qwen_image_params.zero_cond_t) { + modulate_index_vec.clear(); + + int64_t h_len = ((x->ne[1] + (qwen_image_params.patch_size / 2)) / qwen_image_params.patch_size); + int64_t w_len = ((x->ne[0] + (qwen_image_params.patch_size / 2)) / qwen_image_params.patch_size); + int64_t num_img_tokens = h_len * w_len; + + modulate_index_vec.insert(modulate_index_vec.end(), num_img_tokens, 0.f); + int64_t num_ref_img_tokens = 0; + for (ggml_tensor* ref : ref_latents) { + int64_t h_len = ((ref->ne[1] + (qwen_image_params.patch_size / 2)) / qwen_image_params.patch_size); + int64_t w_len = ((ref->ne[0] + (qwen_image_params.patch_size / 2)) / qwen_image_params.patch_size); + + num_ref_img_tokens += h_len * w_len; + } + + if (num_ref_img_tokens > 0) { + modulate_index_vec.insert(modulate_index_vec.end(), num_ref_img_tokens, 1.f); + } + + modulate_index = ggml_new_tensor_1d(compute_ctx, GGML_TYPE_F32, modulate_index_vec.size()); + set_backend_tensor_data(modulate_index, modulate_index_vec.data()); + } + auto runner_ctx = get_context(); struct ggml_tensor* out = qwen_image.forward(&runner_ctx, @@ -583,7 +659,8 @@ namespace Qwen { timesteps, context, pe, - ref_latents); + ref_latents, + modulate_index); ggml_build_forward_expand(gf, out); diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 6c783e1d0..02a5ebc5e 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -520,7 +520,8 @@ class StableDiffusionGGML { offload_params_to_cpu, tensor_storage_map, "model.diffusion_model", - version); + version, + sd_ctx_params->qwen_image_zero_cond_t); } else if (sd_version_is_z_image(version)) { cond_stage_model = std::make_shared(clip_backend, offload_params_to_cpu, diff --git a/stable-diffusion.h b/stable-diffusion.h index 53db5100b..3e9faf854 100644 --- a/stable-diffusion.h +++ b/stable-diffusion.h @@ -195,6 +195,7 @@ typedef struct { bool chroma_use_dit_mask; bool chroma_use_t5_mask; int chroma_t5_mask_pad; + bool qwen_image_zero_cond_t; float flow_shift; } sd_ctx_params_t; From 860a78e24868c6fd05371905e5bddfe3a562c018 Mon Sep 17 00:00:00 2001 From: leejet Date: Wed, 24 Dec 2025 23:30:12 +0800 Subject: [PATCH 44/49] fix: avoid crash when using taesd for preview only (#1141) --- examples/server/main.cpp | 6 +-- qwen_image.hpp | 2 +- stable-diffusion.cpp | 79 ++++++++++++++++++++-------------------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/examples/server/main.cpp b/examples/server/main.cpp index 5f5333da5..c540958f8 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -105,9 +105,9 @@ struct SDSvrParams { std::string listen_ip = "127.0.0.1"; int listen_port = 1234; std::string serve_html_path; - bool normal_exit = false; - bool verbose = false; - bool color = false; + bool normal_exit = false; + bool verbose = false; + bool color = false; ArgOptions get_options() { ArgOptions options; diff --git a/qwen_image.hpp b/qwen_image.hpp index 4b29d63d3..bbbd91bc5 100644 --- a/qwen_image.hpp +++ b/qwen_image.hpp @@ -648,7 +648,7 @@ namespace Qwen { modulate_index_vec.insert(modulate_index_vec.end(), num_ref_img_tokens, 1.f); } - modulate_index = ggml_new_tensor_1d(compute_ctx, GGML_TYPE_F32, modulate_index_vec.size()); + modulate_index = ggml_new_tensor_1d(compute_ctx, GGML_TYPE_F32, modulate_index_vec.size()); set_backend_tensor_data(modulate_index, modulate_index_vec.data()); } diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 02a5ebc5e..4b1c00438 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -591,8 +591,8 @@ class StableDiffusionGGML { vae_backend = backend; } - if (sd_version_is_wan(version) || sd_version_is_qwen_image(version)) { - if (!use_tiny_autoencoder) { + if (!use_tiny_autoencoder || sd_ctx_params->tae_preview_only) { + if (sd_version_is_wan(version) || sd_version_is_qwen_image(version)) { first_stage_model = std::make_shared(vae_backend, offload_params_to_cpu, tensor_storage_map, @@ -601,57 +601,56 @@ class StableDiffusionGGML { version); first_stage_model->alloc_params_buffer(); first_stage_model->get_param_tensors(tensors, "first_stage_model"); + } else if (version == VERSION_CHROMA_RADIANCE) { + first_stage_model = std::make_shared(vae_backend, + offload_params_to_cpu); } else { + first_stage_model = std::make_shared(vae_backend, + offload_params_to_cpu, + tensor_storage_map, + "first_stage_model", + vae_decode_only, + false, + version); + if (sd_ctx_params->vae_conv_direct) { + LOG_INFO("Using Conv2d direct in the vae model"); + first_stage_model->set_conv2d_direct_enabled(true); + } + if (version == VERSION_SDXL && + (strlen(SAFE_STR(sd_ctx_params->vae_path)) == 0 || sd_ctx_params->force_sdxl_vae_conv_scale)) { + float vae_conv_2d_scale = 1.f / 32.f; + LOG_WARN( + "No VAE specified with --vae or --force-sdxl-vae-conv-scale flag set, " + "using Conv2D scale %.3f", + vae_conv_2d_scale); + first_stage_model->set_conv2d_scale(vae_conv_2d_scale); + } + first_stage_model->alloc_params_buffer(); + first_stage_model->get_param_tensors(tensors, "first_stage_model"); + } + } + + if (use_tiny_autoencoder) { + if (sd_version_is_wan(version) || sd_version_is_qwen_image(version)) { tae_first_stage = std::make_shared(vae_backend, offload_params_to_cpu, tensor_storage_map, "decoder", vae_decode_only, version); - if (sd_ctx_params->vae_conv_direct) { - LOG_INFO("Using Conv2d direct in the tae model"); - tae_first_stage->set_conv2d_direct_enabled(true); - } - } - } else if (version == VERSION_CHROMA_RADIANCE) { - first_stage_model = std::make_shared(vae_backend, - offload_params_to_cpu); - } else if (!use_tiny_autoencoder || sd_ctx_params->tae_preview_only) { - first_stage_model = std::make_shared(vae_backend, - offload_params_to_cpu, - tensor_storage_map, - "first_stage_model", - vae_decode_only, - false, - version); - if (sd_ctx_params->vae_conv_direct) { - LOG_INFO("Using Conv2d direct in the vae model"); - first_stage_model->set_conv2d_direct_enabled(true); - } - if (version == VERSION_SDXL && - (strlen(SAFE_STR(sd_ctx_params->vae_path)) == 0 || sd_ctx_params->force_sdxl_vae_conv_scale)) { - float vae_conv_2d_scale = 1.f / 32.f; - LOG_WARN( - "No VAE specified with --vae or --force-sdxl-vae-conv-scale flag set, " - "using Conv2D scale %.3f", - vae_conv_2d_scale); - first_stage_model->set_conv2d_scale(vae_conv_2d_scale); + } else { + tae_first_stage = std::make_shared(vae_backend, + offload_params_to_cpu, + tensor_storage_map, + "decoder.layers", + vae_decode_only, + version); } - first_stage_model->alloc_params_buffer(); - first_stage_model->get_param_tensors(tensors, "first_stage_model"); - } else if (use_tiny_autoencoder) { - tae_first_stage = std::make_shared(vae_backend, - offload_params_to_cpu, - tensor_storage_map, - "decoder.layers", - vae_decode_only, - version); if (sd_ctx_params->vae_conv_direct) { LOG_INFO("Using Conv2d direct in the tae model"); tae_first_stage->set_conv2d_direct_enabled(true); } } - // first_stage_model->get_param_tensors(tensors, "first_stage_model."); if (strlen(SAFE_STR(sd_ctx_params->control_net_path)) > 0) { ggml_backend_t controlnet_backend = nullptr; From df4efe26bd3fc76d6e64d0a946ba96075ba16aeb Mon Sep 17 00:00:00 2001 From: Weiqi Gao Date: Fri, 26 Dec 2025 22:06:13 +0800 Subject: [PATCH 45/49] feat: add png sequence output for vid_gen (#1117) --- examples/cli/README.md | 3 +- examples/cli/main.cpp | 76 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/examples/cli/README.md b/examples/cli/README.md index 568f29d04..c3496aa5b 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -4,7 +4,8 @@ usage: ./bin/sd-cli [options] CLI Options: - -o, --output path to write result image to (default: ./output.png) + -o, --output path to write result image to. you can use printf-style %d format specifiers for image sequences (default: ./output.png) (eg. output_%03d.png) + --output-begin-idx starting index for output image sequence, must be non-negative (default 0 if specified %d in output path, 1 otherwise) --preview-path path to write preview image to (default: ./preview.png) --preview-interval interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at every step) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 7fe3b7692..77ef1c935 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -26,9 +26,12 @@ const char* previews_str[] = { "vae", }; +std::regex format_specifier_regex("(?:[^%]|^)(?:%%)*(%\\d{0,3}d)"); + struct SDCliParams { SDMode mode = IMG_GEN; std::string output_path = "output.png"; + int output_begin_idx = -1; bool verbose = false; bool canny_preprocess = false; @@ -50,7 +53,7 @@ struct SDCliParams { options.string_options = { {"-o", "--output", - "path to write result image to (default: ./output.png)", + "path to write result image to. you can use printf-style %d format specifiers for image sequences (default: ./output.png) (eg. output_%03d.png)", &output_path}, {"", "--preview-path", @@ -63,6 +66,10 @@ struct SDCliParams { "--preview-interval", "interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at every step)", &preview_interval}, + {"", + "--output-begin-idx", + "starting index for output image sequence, must be non-negative (default 0 if specified %d in output path, 1 otherwise)", + &output_begin_idx}, }; options.bool_options = { @@ -344,6 +351,25 @@ void step_callback(int step, int frame_count, sd_image_t* image, bool is_noisy, } } +std::string format_frame_idx(std::string pattern, int frame_idx) { + std::smatch match; + std::string result = pattern; + while (std::regex_search(result, match, format_specifier_regex)) { + std::string specifier = match.str(1); + char buffer[32]; + snprintf(buffer, sizeof(buffer), specifier.c_str(), frame_idx); + result.replace(match.position(1), match.length(1), buffer); + } + + // Then replace all '%%' with '%' + size_t pos = 0; + while ((pos = result.find("%%", pos)) != std::string::npos) { + result.replace(pos, 2, "%"); + pos += 1; + } + return result; +} + int main(int argc, const char* argv[]) { if (argc > 1 && std::string(argv[1]) == "--version") { std::cout << version_string() << "\n"; @@ -719,25 +745,59 @@ int main(int argc, const char* argv[]) { is_jpg = false; } - if (cli_params.mode == VID_GEN && num_results > 1) { - std::string vid_output_path = cli_params.output_path; - if (file_ext_lower == ".png") { - vid_output_path = base_path + ".avi"; + if (std::regex_search(cli_params.output_path, format_specifier_regex)) { + std::string final_output_path = cli_params.output_path; + if (cli_params.output_begin_idx == -1) { + cli_params.output_begin_idx = 0; + } + // writing image sequence, default to PNG + if (!is_jpg && file_ext_lower != ".png") { + base_path += file_ext; + file_ext = ".png"; } - create_mjpg_avi_from_sd_images(vid_output_path.c_str(), results, num_results, gen_params.fps); - LOG_INFO("save result MJPG AVI video to '%s'\n", vid_output_path.c_str()); + final_output_path = base_path + file_ext; + for (int i = 0; i < num_results; i++) { + if (results[i].data == nullptr) { + continue; + } + std::string final_image_path = format_frame_idx(final_output_path, cli_params.output_begin_idx + i); + if (is_jpg) { + int write_ok = stbi_write_jpg(final_image_path.c_str(), results[i].width, results[i].height, results[i].channel, + results[i].data, 90, get_image_params(cli_params, ctx_params, gen_params, gen_params.seed + i).c_str()); + LOG_INFO("save result JPEG image %d to '%s' (%s)", i, final_image_path.c_str(), write_ok == 0 ? "failure" : "success"); + } else { + int write_ok = stbi_write_png(final_image_path.c_str(), results[i].width, results[i].height, results[i].channel, + results[i].data, 0, get_image_params(cli_params, ctx_params, gen_params, gen_params.seed + i).c_str()); + LOG_INFO("save result PNG image %d to '%s' (%s)", i, final_image_path.c_str(), write_ok == 0 ? "failure" : "success"); + } + } + } else if (cli_params.mode == VID_GEN && num_results > 1) { + std::string final_output_path = cli_params.output_path; + if (file_ext_lower != ".avi") { + if (!is_jpg && file_ext_lower != ".png") { + base_path += file_ext; + } + file_ext = ".avi"; + final_output_path = base_path + file_ext; + } + create_mjpg_avi_from_sd_images(final_output_path.c_str(), results, num_results, gen_params.fps); + LOG_INFO("save result MJPG AVI video to '%s'\n", final_output_path.c_str()); } else { // appending ".png" to absent or unknown extension if (!is_jpg && file_ext_lower != ".png") { base_path += file_ext; file_ext = ".png"; } + if (cli_params.output_begin_idx == -1) { + cli_params.output_begin_idx = 1; + } for (int i = 0; i < num_results; i++) { if (results[i].data == nullptr) { continue; } int write_ok; - std::string final_image_path = i > 0 ? base_path + "_" + std::to_string(i + 1) + file_ext : base_path + file_ext; + std::string final_image_path; + final_image_path = i > 0 ? base_path + "_" + std::to_string(cli_params.output_begin_idx + i) + file_ext : base_path + file_ext; if (is_jpg) { write_ok = stbi_write_jpg(final_image_path.c_str(), results[i].width, results[i].height, results[i].channel, results[i].data, 90, get_image_params(cli_params, ctx_params, gen_params, gen_params.seed + i).c_str()); From ccb6b0ac9def57e483b3d373e577e06edf031e04 Mon Sep 17 00:00:00 2001 From: leejet Date: Fri, 26 Dec 2025 22:07:40 +0800 Subject: [PATCH 46/49] feat: add __index_timestep_zero__ support (#1146) --- qwen_image.hpp | 7 ++++++- stable-diffusion.cpp | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qwen_image.hpp b/qwen_image.hpp index bbbd91bc5..9c095a9a0 100644 --- a/qwen_image.hpp +++ b/qwen_image.hpp @@ -561,11 +561,13 @@ namespace Qwen { : GGMLRunner(backend, offload_params_to_cpu) { qwen_image_params.num_layers = 0; qwen_image_params.zero_cond_t = zero_cond_t; - LOG_DEBUG("zero_cond_t: %d", zero_cond_t); for (auto pair : tensor_storage_map) { std::string tensor_name = pair.first; if (tensor_name.find(prefix) == std::string::npos) continue; + if (tensor_name.find("__index_timestep_zero__") != std::string::npos) { + qwen_image_params.zero_cond_t = true; + } size_t pos = tensor_name.find("transformer_blocks."); if (pos != std::string::npos) { tensor_name = tensor_name.substr(pos); // remove prefix @@ -580,6 +582,9 @@ namespace Qwen { } } LOG_INFO("qwen_image_params.num_layers: %ld", qwen_image_params.num_layers); + if (qwen_image_params.zero_cond_t) { + LOG_INFO("use zero_cond_t"); + } qwen_image = QwenImageModel(qwen_image_params); qwen_image.init(params_ctx, tensor_storage_map, prefix); } diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 4b1c00438..58d420415 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -750,6 +750,7 @@ class StableDiffusionGGML { } ignore_tensors.insert("model.diffusion_model.__x0__"); ignore_tensors.insert("model.diffusion_model.__32x32__"); + ignore_tensors.insert("model.diffusion_model.__index_timestep_zero__"); if (vae_decode_only) { ignore_tensors.insert("first_stage_model.encoder"); From 37c9860b795492409f3ad2edbca97929c171edb2 Mon Sep 17 00:00:00 2001 From: leejet Date: Sat, 27 Dec 2025 15:43:19 +0800 Subject: [PATCH 47/49] fix: handle redirected UTF-8 output correctly on Windows (#1147) --- examples/common/common.hpp | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/examples/common/common.hpp b/examples/common/common.hpp index 74560814e..cdc8f181f 100644 --- a/examples/common/common.hpp +++ b/examples/common/common.hpp @@ -95,17 +95,28 @@ static void print_utf8(FILE* stream, const char* utf8) { ? GetStdHandle(STD_ERROR_HANDLE) : GetStdHandle(STD_OUTPUT_HANDLE); - int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); - if (wlen <= 0) - return; + DWORD mode; + BOOL is_console = GetConsoleMode(h, &mode); + + if (is_console) { + int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); + if (wlen <= 0) + return; - wchar_t* wbuf = (wchar_t*)malloc(wlen * sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wbuf, wlen); + wchar_t* wbuf = (wchar_t*)malloc(wlen * sizeof(wchar_t)); + if (!wbuf) + return; - DWORD written; - WriteConsoleW(h, wbuf, wlen - 1, &written, NULL); + MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wbuf, wlen); - free(wbuf); + DWORD written; + WriteConsoleW(h, wbuf, wlen - 1, &written, NULL); + + free(wbuf); + } else { + DWORD written; + WriteFile(h, utf8, (DWORD)strlen(utf8), &written, NULL); + } #else fputs(utf8, stream); #endif From cc107714d7f17f7baa9976020d333fb54ce489a7 Mon Sep 17 00:00:00 2001 From: Wagner Bruna Date: Sat, 27 Dec 2025 04:54:18 -0300 Subject: [PATCH 48/49] fix: consistently pass 2nd-order samplers half steps as negatives (#1095) --- denoiser.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/denoiser.hpp b/denoiser.hpp index 7a8242e7d..2dc4c60e1 100644 --- a/denoiser.hpp +++ b/denoiser.hpp @@ -869,7 +869,7 @@ static bool sample_k_diffusion(sample_method_t method, for (int i = 0; i < steps; i++) { // denoise - ggml_tensor* denoised = model(x, sigmas[i], i + 1); + ggml_tensor* denoised = model(x, sigmas[i], -(i + 1)); if (denoised == nullptr) { return false; } @@ -927,7 +927,7 @@ static bool sample_k_diffusion(sample_method_t method, for (int i = 0; i < steps; i++) { // denoise - ggml_tensor* denoised = model(x, sigmas[i], i + 1); + ggml_tensor* denoised = model(x, sigmas[i], -(i + 1)); if (denoised == nullptr) { return false; } From a2d83dd0c804dc39d7ceb2e599cfbb02488da8e9 Mon Sep 17 00:00:00 2001 From: leejet Date: Sat, 27 Dec 2025 16:48:15 +0800 Subject: [PATCH 49/49] refactor: move pmid condition logic into get_pmid_condition (#1148) --- stable-diffusion.cpp | 201 ++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 109 deletions(-) diff --git a/stable-diffusion.cpp b/stable-diffusion.cpp index 58d420415..9a97d4c74 100644 --- a/stable-diffusion.cpp +++ b/stable-diffusion.cpp @@ -129,7 +129,7 @@ class StableDiffusionGGML { bool use_tiny_autoencoder = false; sd_tiling_params_t vae_tiling_params = {false, 0, 0, 0.5f, 0, 0}; bool offload_params_to_cpu = false; - bool stacked_id = false; + bool use_pmid = false; bool is_using_v_parameterization = false; bool is_using_edm_v_parameterization = false; @@ -701,10 +701,10 @@ class StableDiffusionGGML { if (!model_loader.init_from_file_and_convert_name(sd_ctx_params->photo_maker_path, "pmid.")) { LOG_WARN("loading stacked ID embedding from '%s' failed", sd_ctx_params->photo_maker_path); } else { - stacked_id = true; + use_pmid = true; } } - if (stacked_id) { + if (use_pmid) { if (!pmid_model->alloc_params_buffer()) { LOG_ERROR(" pmid model params buffer allocation failed"); return false; @@ -745,7 +745,7 @@ class StableDiffusionGGML { if (use_tiny_autoencoder) { ignore_tensors.insert("first_stage_model."); } - if (stacked_id) { + if (use_pmid) { ignore_tensors.insert("pmid.unet."); } ignore_tensors.insert("model.diffusion_model.__x0__"); @@ -799,7 +799,7 @@ class StableDiffusionGGML { control_net_params_mem_size = control_net->get_params_buffer_size(); } size_t pmid_params_mem_size = 0; - if (stacked_id) { + if (use_pmid) { pmid_params_mem_size = pmid_model->get_params_buffer_size(); } @@ -1211,14 +1211,89 @@ class StableDiffusionGGML { } } - ggml_tensor* id_encoder(ggml_context* work_ctx, - ggml_tensor* init_img, - ggml_tensor* prompts_embeds, - ggml_tensor* id_embeds, - std::vector& class_tokens_mask) { - ggml_tensor* res = nullptr; - pmid_model->compute(n_threads, init_img, prompts_embeds, id_embeds, class_tokens_mask, &res, work_ctx); - return res; + SDCondition get_pmid_conditon(ggml_context* work_ctx, + sd_pm_params_t pm_params, + ConditionerParams& condition_params) { + SDCondition id_cond; + if (use_pmid) { + if (!pmid_lora->applied) { + int64_t t0 = ggml_time_ms(); + pmid_lora->apply(tensors, version, n_threads); + int64_t t1 = ggml_time_ms(); + pmid_lora->applied = true; + LOG_INFO("pmid_lora apply completed, taking %.2fs", (t1 - t0) * 1.0f / 1000); + if (free_params_immediately) { + pmid_lora->free_params_buffer(); + } + } + // preprocess input id images + bool pmv2 = pmid_model->get_version() == PM_VERSION_2; + if (pm_params.id_images_count > 0) { + int clip_image_size = 224; + pmid_model->style_strength = pm_params.style_strength; + + auto id_image_tensor = ggml_new_tensor_4d(work_ctx, GGML_TYPE_F32, clip_image_size, clip_image_size, 3, pm_params.id_images_count); + + std::vector processed_id_images; + for (int i = 0; i < pm_params.id_images_count; i++) { + sd_image_f32_t id_image = sd_image_t_to_sd_image_f32_t(pm_params.id_images[i]); + sd_image_f32_t processed_id_image = clip_preprocess(id_image, clip_image_size, clip_image_size); + free(id_image.data); + id_image.data = nullptr; + processed_id_images.push_back(processed_id_image); + } + + ggml_ext_tensor_iter(id_image_tensor, [&](ggml_tensor* id_image_tensor, int64_t i0, int64_t i1, int64_t i2, int64_t i3) { + float value = sd_image_get_f32(processed_id_images[i3], i0, i1, i2, false); + ggml_ext_tensor_set_f32(id_image_tensor, value, i0, i1, i2, i3); + }); + + for (auto& image : processed_id_images) { + free(image.data); + image.data = nullptr; + } + processed_id_images.clear(); + + int64_t t0 = ggml_time_ms(); + condition_params.num_input_imgs = pm_params.id_images_count; + auto cond_tup = cond_stage_model->get_learned_condition_with_trigger(work_ctx, + n_threads, + condition_params); + id_cond = std::get<0>(cond_tup); + auto class_tokens_mask = std::get<1>(cond_tup); + struct ggml_tensor* id_embeds = nullptr; + if (pmv2 && pm_params.id_embed_path != nullptr) { + id_embeds = load_tensor_from_file(work_ctx, pm_params.id_embed_path); + } + if (pmv2 && id_embeds == nullptr) { + LOG_WARN("Provided PhotoMaker images, but NO valid ID embeds file for PM v2"); + LOG_WARN("Turn off PhotoMaker"); + use_pmid = false; + } else { + if (pmv2 && pm_params.id_images_count != id_embeds->ne[1]) { + LOG_WARN("PhotoMaker image count (%d) does NOT match ID embeds (%d). You should run face_detect.py again.", pm_params.id_images_count, id_embeds->ne[1]); + LOG_WARN("Turn off PhotoMaker"); + use_pmid = false; + } else { + ggml_tensor* res = nullptr; + pmid_model->compute(n_threads, id_image_tensor, id_cond.c_crossattn, id_embeds, class_tokens_mask, &res, work_ctx); + id_cond.c_crossattn = res; + int64_t t1 = ggml_time_ms(); + LOG_INFO("Photomaker ID Stacking, taking %" PRId64 " ms", t1 - t0); + if (free_params_immediately) { + pmid_model->free_params_buffer(); + } + // Encode input prompt without the trigger word for delayed conditioning + condition_params.text = cond_stage_model->remove_trigger_from_prompt(work_ctx, condition_params.text); + } + } + } else { + LOG_WARN("Provided PhotoMaker model file, but NO input ID images"); + LOG_WARN("Turn off PhotoMaker"); + use_pmid = false; + } + } + return id_cond; } ggml_tensor* get_clip_vision_output(ggml_context* work_ctx, @@ -3117,114 +3192,22 @@ sd_image_t* generate_image_internal(sd_ctx_t* sd_ctx, guidance.img_cfg = guidance.txt_cfg; } - // for (auto v : sigmas) { - // std::cout << v << " "; - // } - // std::cout << std::endl; - int sample_steps = sigmas.size() - 1; int64_t t0 = ggml_time_ms(); - // Photo Maker - std::string prompt_text_only; - ggml_tensor* init_img = nullptr; - SDCondition id_cond; - std::vector class_tokens_mask; - ConditionerParams condition_params; + condition_params.text = prompt; condition_params.clip_skip = clip_skip; condition_params.width = width; condition_params.height = height; condition_params.ref_images = ref_images; condition_params.adm_in_channels = sd_ctx->sd->diffusion_model->get_adm_in_channels(); - if (sd_ctx->sd->stacked_id) { - if (!sd_ctx->sd->pmid_lora->applied) { - int64_t t0 = ggml_time_ms(); - sd_ctx->sd->pmid_lora->apply(sd_ctx->sd->tensors, sd_ctx->sd->version, sd_ctx->sd->n_threads); - int64_t t1 = ggml_time_ms(); - sd_ctx->sd->pmid_lora->applied = true; - LOG_INFO("pmid_lora apply completed, taking %.2fs", (t1 - t0) * 1.0f / 1000); - if (sd_ctx->sd->free_params_immediately) { - sd_ctx->sd->pmid_lora->free_params_buffer(); - } - } - // preprocess input id images - bool pmv2 = sd_ctx->sd->pmid_model->get_version() == PM_VERSION_2; - if (pm_params.id_images_count > 0) { - int clip_image_size = 224; - sd_ctx->sd->pmid_model->style_strength = pm_params.style_strength; - - init_img = ggml_new_tensor_4d(work_ctx, GGML_TYPE_F32, clip_image_size, clip_image_size, 3, pm_params.id_images_count); - - std::vector processed_id_images; - for (int i = 0; i < pm_params.id_images_count; i++) { - sd_image_f32_t id_image = sd_image_t_to_sd_image_f32_t(pm_params.id_images[i]); - sd_image_f32_t processed_id_image = clip_preprocess(id_image, clip_image_size, clip_image_size); - free(id_image.data); - id_image.data = nullptr; - processed_id_images.push_back(processed_id_image); - } - - ggml_ext_tensor_iter(init_img, [&](ggml_tensor* init_img, int64_t i0, int64_t i1, int64_t i2, int64_t i3) { - float value = sd_image_get_f32(processed_id_images[i3], i0, i1, i2, false); - ggml_ext_tensor_set_f32(init_img, value, i0, i1, i2, i3); - }); - - for (auto& image : processed_id_images) { - free(image.data); - image.data = nullptr; - } - processed_id_images.clear(); - - int64_t t0 = ggml_time_ms(); - condition_params.text = prompt; - condition_params.num_input_imgs = pm_params.id_images_count; - auto cond_tup = sd_ctx->sd->cond_stage_model->get_learned_condition_with_trigger(work_ctx, - sd_ctx->sd->n_threads, - condition_params); - id_cond = std::get<0>(cond_tup); - class_tokens_mask = std::get<1>(cond_tup); // - struct ggml_tensor* id_embeds = nullptr; - if (pmv2 && pm_params.id_embed_path != nullptr) { - id_embeds = load_tensor_from_file(work_ctx, pm_params.id_embed_path); - // print_ggml_tensor(id_embeds, true, "id_embeds:"); - } - if (pmv2 && id_embeds == nullptr) { - LOG_WARN("Provided PhotoMaker images, but NO valid ID embeds file for PM v2"); - LOG_WARN("Turn off PhotoMaker"); - sd_ctx->sd->stacked_id = false; - } else { - if (pmv2 && pm_params.id_images_count != id_embeds->ne[1]) { - LOG_WARN("PhotoMaker image count (%d) does NOT match ID embeds (%d). You should run face_detect.py again.", pm_params.id_images_count, id_embeds->ne[1]); - LOG_WARN("Turn off PhotoMaker"); - sd_ctx->sd->stacked_id = false; - } else { - id_cond.c_crossattn = sd_ctx->sd->id_encoder(work_ctx, init_img, id_cond.c_crossattn, id_embeds, class_tokens_mask); - int64_t t1 = ggml_time_ms(); - LOG_INFO("Photomaker ID Stacking, taking %" PRId64 " ms", t1 - t0); - if (sd_ctx->sd->free_params_immediately) { - sd_ctx->sd->pmid_model->free_params_buffer(); - } - // Encode input prompt without the trigger word for delayed conditioning - prompt_text_only = sd_ctx->sd->cond_stage_model->remove_trigger_from_prompt(work_ctx, prompt); - // printf("%s || %s \n", prompt.c_str(), prompt_text_only.c_str()); - prompt = prompt_text_only; // - if (sample_steps < 50) { - LOG_WARN("It's recommended to use >= 50 steps for photo maker!"); - } - } - } - } else { - LOG_WARN("Provided PhotoMaker model file, but NO input ID images"); - LOG_WARN("Turn off PhotoMaker"); - sd_ctx->sd->stacked_id = false; - } - } + // Photo Maker + SDCondition id_cond = sd_ctx->sd->get_pmid_conditon(work_ctx, pm_params, condition_params); // Get learned condition - condition_params.text = prompt; condition_params.zero_out_masked = false; SDCondition cond = sd_ctx->sd->cond_stage_model->get_learned_condition(work_ctx, sd_ctx->sd->n_threads, @@ -3364,7 +3347,7 @@ sd_image_t* generate_image_internal(sd_ctx_t* sd_ctx, ggml_ext_im_set_randn_f32(noise, sd_ctx->sd->rng); int start_merge_step = -1; - if (sd_ctx->sd->stacked_id) { + if (sd_ctx->sd->use_pmid) { start_merge_step = int(sd_ctx->sd->pmid_model->style_strength / 100.f * sample_steps); // if (start_merge_step > 30) // start_merge_step = 30;