diff --git a/IccConnect/IccLibConnect/IccConnect.cpp b/IccConnect/IccLibConnect/IccConnect.cpp index 440c1ec12..27a848adf 100644 --- a/IccConnect/IccLibConnect/IccConnect.cpp +++ b/IccConnect/IccLibConnect/IccConnect.cpp @@ -105,7 +105,12 @@ CIccNamedColorCmm* CIccConnectCmm::GetNamedCmm() const CIccCmmSearch* CIccConnectCmm::GetSearchCmm() const { - return dynamic_cast(m_pCmm); + CIccCmmSearch* search = dynamic_cast(m_pCmm); + if (search) + return search; + + CIccThreadedCmm* threaded = GetThreadedCmm(); + return threaded ? dynamic_cast(threaded->GetBaseCmm()) : nullptr; } CIccThreadedCmm* CIccConnectCmm::GetThreadedCmm() const @@ -462,10 +467,25 @@ CIccConnectCmm* CIccConnectCmm::CreateStandard(const CIccCfgProfileSequence& pro CIccConnectCmm* CIccConnectCmm::CreateSearch(const CIccCfgSearchApply& searchApply, std::string* pErrorMsg) +{ + return CreateSearch(searchApply, pErrorMsg, 1); +} + +CIccConnectCmm* CIccConnectCmm::CreateSearch(const CIccCfgSearchApply& searchApply, + std::string* pErrorMsg, + int nThreads) { std::string localErr; std::string& sErrorMsg = pErrorMsg ? *pErrorMsg : localErr; + if (nThreads < 0 || nThreads > CIccThreadedCmm::GetMaxThreads()) { + std::ostringstream oss; + oss << "invalid thread count " << nThreads << " (maximum " + << CIccThreadedCmm::GetMaxThreads() << ")"; + sErrorMsg = oss.str(); + return nullptr; + } + IccProfilePtrList pccList; auto pCmm = std::unique_ptr(new (std::nothrow) CIccCmmSearch()); if (!pCmm) { @@ -633,7 +653,11 @@ CIccConnectCmm* CIccConnectCmm::CreateSearch(const CIccCfgSearchApply& searchApp } ReleasePccList(pccList); - return Attach(pCmm.release()); + std::unique_ptr baseCmm(pCmm.release()); + CIccConnectCmm* pConnect = AttachStandardCmm(baseCmm, nThreads); + if (!pConnect) + sErrorMsg = "failed to attach search CMM"; + return pConnect; } #ifdef USEICCDEVNAMESPACE diff --git a/IccConnect/IccLibConnect/IccConnect.h b/IccConnect/IccLibConnect/IccConnect.h index d2b27627c..7ccb58dd6 100644 --- a/IccConnect/IccLibConnect/IccConnect.h +++ b/IccConnect/IccLibConnect/IccConnect.h @@ -119,6 +119,9 @@ class CIccConnectCmm // description of the first failure encountered. Callers print it. static CIccConnectCmm* CreateSearch(const CIccCfgSearchApply& searchApply, std::string* pErrorMsg = nullptr); + static CIccConnectCmm* CreateSearch(const CIccCfgSearchApply& searchApply, + std::string* pErrorMsg, + int nThreads); // Wraps an already-initialized CMM (takes ownership). static CIccConnectCmm* Attach(CIccCmm* pCmm); diff --git a/IccProfLib/IccCmmSearch.cpp b/IccProfLib/IccCmmSearch.cpp index a64c1a731..8e9c70c62 100644 --- a/IccProfLib/IccCmmSearch.cpp +++ b/IccProfLib/IccCmmSearch.cpp @@ -71,12 +71,17 @@ #include "IccCmmSearch.h" #include +#include +#include CIccApplyCmmSearch::CIccApplyCmmSearch(CIccCmm* pBaseCmm) : CIccApplyCmm(pBaseCmm) { CIccCmmSearch* pCmm = (CIccCmmSearch*)pBaseCmm; + m_bReady = false; + m_pMidToDstApply = nullptr; + m_nApply = pCmm->m_pcc.size(); if (!m_nApply) m_nApply = 1; @@ -113,10 +118,50 @@ CIccApplyCmmSearch::CIccApplyCmmSearch(CIccCmm* pBaseCmm) : CIccApplyCmm(pBaseCm m_maxBounds = pCmm->m_maxBounds; m_bNeedPcsToLab = pCmm->m_bNeedPcsToLab; + + icStatusCMM status = icCmmStatOk; + std::unique_ptr mid_to_dst_apply( + pCmm->m_mid_to_dst->GetNewApplyCmm(status)); + if (!mid_to_dst_apply || status != icCmmStatOk) + return; + + std::vector> src_to_mid_apply; + src_to_mid_apply.reserve(pCmm->m_src_to_mid.size()); + for (const auto& cmm : pCmm->m_src_to_mid) { + std::unique_ptr apply(cmm->GetNewApplyCmm(status)); + if (!apply || status != icCmmStatOk) { + return; + } + src_to_mid_apply.push_back(std::move(apply)); + } + + std::vector> dst_to_mid_apply; + dst_to_mid_apply.reserve(pCmm->m_dst_to_mid.size()); + for (const auto& cmm : pCmm->m_dst_to_mid) { + std::unique_ptr apply(cmm->GetNewApplyCmm(status)); + if (!apply || status != icCmmStatOk) { + return; + } + dst_to_mid_apply.push_back(std::move(apply)); + } + + m_srcToMidApply.reserve(src_to_mid_apply.size()); + m_dstToMidApply.reserve(dst_to_mid_apply.size()); + m_pMidToDstApply = mid_to_dst_apply.release(); + for (auto& apply : src_to_mid_apply) + m_srcToMidApply.push_back(apply.release()); + for (auto& apply : dst_to_mid_apply) + m_dstToMidApply.push_back(apply.release()); + m_bReady = true; } CIccApplyCmmSearch::~CIccApplyCmmSearch() { + delete m_pMidToDstApply; + for (CIccApplyCmm* apply : m_srcToMidApply) + delete apply; + for (CIccApplyCmm* apply : m_dstToMidApply) + delete apply; } static icFloatNumber sq(icFloatNumber x) { return x * x; } @@ -133,7 +178,7 @@ icFloatNumber CIccApplyCmmSearch::costFunc(CIccSearchVec& point) // costFunc has no status channel, so report the point as infeasible instead: // the same sentinel the bounds barrier uses keeps the optimizer away from it // rather than letting it converge on a garbage minimum. - if (pCmm->m_dst_to_mid[i]->Apply(&m_pixel[0], &point.vec()[0]) != icCmmStatOk) + if (m_dstToMidApply[i]->Apply(&m_pixel[0], &point.vec()[0]) != icCmmStatOk) return overBoundsCost; if (m_bNeedPcsToLab) { @@ -200,7 +245,7 @@ icStatusCMM CIccApplyCmmSearch::Apply(icFloatNumber* DstPixel, const icFloatNumb // Propagate the per-PCC forward transform status (#1860). Swallowing it // left m_mid_data[i] holding whatever the previous pixel wrote, so the // search then optimised against a stale target and still reported success. - icStatusCMM statMid = pCmm->m_src_to_mid[i]->Apply(&m_mid_data[i][0], SrcPixel); + icStatusCMM statMid = m_srcToMidApply[i]->Apply(&m_mid_data[i][0], SrcPixel); if (statMid != icCmmStatOk) return statMid; } @@ -208,7 +253,7 @@ icStatusCMM CIccApplyCmmSearch::Apply(icFloatNumber* DstPixel, const icFloatNumb // Same for the transform that seeds the search's starting point: if it fails // m_startPixel is stale and every subsequent simplex vertex derives from it. - icStatusCMM statStart = pCmm->m_mid_to_dst->Apply(&m_startPixel[0], &m_mid_data[0][0]); + icStatusCMM statStart = m_pMidToDstApply->Apply(&m_startPixel[0], &m_mid_data[0][0]); if (statStart != icCmmStatOk) return statStart; @@ -374,7 +419,7 @@ icStatusCMM CIccCmmSearch::AttachPCC(IIccProfileConnectionConditions* pPCC, icFl #define checkCmmStatus(rv) if (rv != icCmmStatOk) return rv -icStatusCMM CIccCmmSearch::Begin(bool /* bAllocNewApply */, bool /* bUsePcsConversion */) +icStatusCMM CIccCmmSearch::Begin(bool bAllocNewApply, bool /* bUsePcsConversion */) { icStatusCMM rv; @@ -390,8 +435,17 @@ icStatusCMM CIccCmmSearch::Begin(bool /* bAllocNewApply */, bool /* bUsePcsConve // the reference AddXform overload. Guarding on m_pApply -- which the tail of // this function sets -- also stops that second pass leaking the CIccApplyCmm // the first one allocated and pushing a duplicate chain into m_dst_to_mid. - if (m_pApply) + if (m_bValid) { + if (bAllocNewApply && !m_pApply) { + m_pApply = GetNewApplyCmm(rv); + if (!m_pApply || rv != icCmmStatOk) { + delete m_pApply; + m_pApply = nullptr; + return rv; + } + } return icCmmStatOk; + } if (m_nAttached < 2) return icCmmStatBadXform; @@ -553,13 +607,49 @@ icStatusCMM CIccCmmSearch::Begin(bool /* bAllocNewApply */, bool /* bUsePcsConve else m_bNeedPcsToLab = false; - m_pApply = new CIccApplyCmmSearch(this); - m_bValid = true; + if (bAllocNewApply) { + m_pApply = GetNewApplyCmm(rv); + if (!m_pApply || rv != icCmmStatOk) { + delete m_pApply; + m_pApply = nullptr; + return rv; + } + } + return rv; } +CIccApplyCmm* CIccCmmSearch::GetNewApplyCmm(icStatusCMM& status) +{ + if (!m_bValid) { + status = icCmmStatBad; + return nullptr; + } + + CIccApplyCmmSearch* apply = nullptr; + try { + apply = new (std::nothrow) CIccApplyCmmSearch(this); + } + catch (const std::bad_alloc&) { + status = icCmmStatAllocErr; + return nullptr; + } + if (!apply) { + status = icCmmStatAllocErr; + return nullptr; + } + if (!apply->IsReady()) { + delete apply; + status = icCmmStatAllocErr; + return nullptr; + } + + status = icCmmStatOk; + return apply; +} + //Call to Detach and remove all pending IO objects attached to the profiles used by the CMM. Should be called only after Begin() icStatusCMM CIccCmmSearch::RemoveAllIO() { diff --git a/IccProfLib/IccCmmSearch.h b/IccProfLib/IccCmmSearch.h index 7372fa146..59b288c0d 100644 --- a/IccProfLib/IccCmmSearch.h +++ b/IccProfLib/IccCmmSearch.h @@ -128,6 +128,8 @@ class ICCPROFLIB_API CIccApplyCmmSearch : public CIccApplyCmm, public CIccMinSea // Returns icCmmStatOk on success. virtual icStatusCMM GetApplyCost(icFloatNumber& dCost, const icFloatNumber* SrcPixel); + bool IsReady() const { return m_bReady; } + protected: CIccApplyCmmSearch(CIccCmm* pCmm); @@ -143,6 +145,10 @@ class ICCPROFLIB_API CIccApplyCmmSearch : public CIccApplyCmm, public CIccMinSea icFloatVector m_maxBounds; bool m_bNeedPcsToLab; + bool m_bReady; + CIccApplyCmm* m_pMidToDstApply; + std::vector m_srcToMidApply; + std::vector m_dstToMidApply; }; @@ -184,7 +190,7 @@ class ICCPROFLIB_API CIccCmmSearch : public CIccCmm virtual icStatusCMM Begin(bool bAllocNewApply = true, bool bUsePcsConversion = false); //Get an additional Apply CMM object to apply pixels with. The Apply object should be deleted by the caller. - virtual CIccApplyCmm* GetNewApplyCmm(icStatusCMM& /*status*/) { return nullptr; } + virtual CIccApplyCmm* GetNewApplyCmm(icStatusCMM& status); //Call to Detach and remove all pending IO objects attached to the profiles used by the CMM. Should be called only after Begin() virtual icStatusCMM RemoveAllIO(); diff --git a/IccProfLib/IccCmmThread.h b/IccProfLib/IccCmmThread.h index 5c18f2052..d7564376e 100644 --- a/IccProfLib/IccCmmThread.h +++ b/IccProfLib/IccCmmThread.h @@ -182,6 +182,7 @@ class ICCPROFLIB_API CIccThreadedCmm : public CIccCmm virtual icColorSpaceSignature GetLastXformDest() { return m_pCmm->GetLastXformDest(); } int GetNumThreads() const { return m_nThreads; } + CIccCmm* GetBaseCmm() const { return m_pCmm; } protected: CIccCmm *m_pCmm; diff --git a/Tools/CmdLine/IccApplySearch/iccApplySearch.cpp b/Tools/CmdLine/IccApplySearch/iccApplySearch.cpp index 1445fb13a..2e27b3ca6 100644 --- a/Tools/CmdLine/IccApplySearch/iccApplySearch.cpp +++ b/Tools/CmdLine/IccApplySearch/iccApplySearch.cpp @@ -70,6 +70,7 @@ #include "IccCmmSearch.h" +#include "IccCmmThread.h" #include "IccUtil.h" #include "IccDefs.h" #include "IccMpeCalc.h" @@ -78,6 +79,7 @@ #include "IccSearch.h" #include "IccConnect.h" #include "IccCmdLineUtil.h" +#include #include // EXIT_FAILURE, used by the -cfg argument guard added in #2075 #include #include @@ -206,9 +208,10 @@ bool IsSpacePCS( const icColorSpaceSignature &x ) void Usage() { printf("iccApplySearch built with IccProfLib version " ICCPROFLIBVER ", IccLibConnect Version " ICCLIBCONNECTVER "\n\n"); - printf("Usage 1: iccApplySearch -cfg config_file_path\n"); + printf("Usage 1: iccApplySearch {-threads N} -cfg config_file_path\n"); + printf(" Optional: -threads N (0=hardware concurrency, 1=single-threaded)\n"); printf(" Where config_file_path is a json formatted ICC profile application configuration file\n\n"); - printf("Usage 2: iccApplySearch {-debugcalc} data_file_path encoding[:precision[:digits]] interpolation {-ENV:tag value} profile1_path intent1 {{-ENV:tag value} middle_profile_path mid_intent} {-ENV:tag value} profile2_path intent2 -INIT init_intent2 {pcc_path1 weight1 ...}\n"); + printf("Usage 2: iccApplySearch {-threads N} {-debugcalc} data_file_path encoding[:precision[:digits]] interpolation {-ENV:tag value} profile1_path intent1 {{-ENV:tag value} middle_profile_path mid_intent} {-ENV:tag value} profile2_path intent2 -INIT init_intent2 {pcc_path1 weight1 ...}\n"); printf(" For final_data_encoding:\n"); printf(" 0 - icEncodeValue (converts to/from lab encoding when samples=3)\n"); @@ -246,6 +249,10 @@ void Usage() int main(int argc, const char* argv[]) { int minargs = 3; // name -cfg file.json + if (argc > 1 && !stricmp(argv[1], "-threads") && argc < 3) { + printf("Missing thread count for -threads\n"); + return EXIT_FAILURE; + } if (argc < minargs) { Usage(); return 0; @@ -254,6 +261,27 @@ int main(int argc, const char* argv[]) CIccCfgDataApply cfgApply; CIccCfgSearchApply cfgSearchApply; CIccCfgColorData cfgData; + int nThreads = 1; + + if (!stricmp(argv[1], "-threads")) { + char* end = nullptr; + errno = 0; + long parsed = strtol(argv[2], &end, 10); + if (errno || !end || end == argv[2] || *end || parsed < 0 || + parsed > CIccThreadedCmm::GetMaxThreads()) { + printf("Invalid thread count '%s': expected 0..%d\n", argv[2], + CIccThreadedCmm::GetMaxThreads()); + return EXIT_FAILURE; + } + nThreads = (int)parsed; + argv += 2; + argc -= 2; + + if (argc < minargs) { + Usage(); + return EXIT_FAILURE; + } + } if (!stricmp(argv[1], "-cfg")) { // Usage 1 is exactly "-cfg "; every setting comes from the JSON file, so @@ -405,6 +433,11 @@ int main(int argc, const char* argv[]) return -1; } + if (cfgApply.m_debugCalc && nThreads != 1) { + printf("-debugcalc requires -threads 1\n"); + return EXIT_FAILURE; + } + LogDebuggerPtr pDebugger; if (cfgApply.m_debugCalc) { @@ -423,7 +456,7 @@ int main(int argc, const char* argv[]) std::string sConnectError; std::unique_ptr pConnect( - CIccConnectCmm::CreateSearch(cfgSearchApply, &sConnectError)); + CIccConnectCmm::CreateSearch(cfgSearchApply, &sConnectError, nThreads)); if (!pConnect) { if (!sConnectError.empty()) @@ -478,6 +511,55 @@ int main(int argc, const char* argv[]) outData.m_srcEncoding = srcEncoding; + if (nThreads != 1) { + std::vector entries; + for (const auto& data : cfgData.m_data) { + if (data) + entries.push_back(data.get()); + } + + std::vector srcPixels(entries.size() * nSrcSamples); + std::vector dstPixels(entries.size() * nDestSamples); + std::vector outputs; + outputs.reserve(entries.size()); + + for (size_t index = 0; index < entries.size(); ++index) { + CIccCfgDataEntry* pData = entries[index]; + CIccCfgDataEntryPtr out(new CIccCfgDataEntry()); + out->m_srcName = pData->m_name; + out->m_srcValues = pData->m_values; + + for (size_t i = 0; i < nSrcSamples && i < pData->m_values.size(); ++i) + Pixel[i] = pData->m_values[i]; + + if (CIccCmm::ToInternalEncoding(SrcspaceSig, srcEncoding, + srcPixels.data() + index * nSrcSamples, + Pixel, bClip)) { + printf("Invalid source data encoding\n"); + return -1; + } + outputs.push_back(out); + } + + if (!entries.empty() && + pConnect->GetCmm()->Apply(dstPixels.data(), srcPixels.data(), + (icUInt32Number)entries.size())) { + printf("Profile application failed.\n"); + return -1; + } + + for (size_t index = 0; index < outputs.size(); ++index) { + icFloatNumber* dst = dstPixels.data() + index * nDestSamples; + if (CIccCmm::FromInternalEncoding(DestspaceSig, destEncoding, dst, dst)) { + printf("Invalid final data encoding\n"); + return -1; + } + for (size_t i = 0; i < nDestSamples; ++i) + outputs[index]->m_values.push_back(dst[i]); + outData.m_data.push_back(outputs[index]); + } + } + else { //Apply profiles to each input color for (auto dataIter = cfgData.m_data.begin(); dataIter != cfgData.m_data.end(); dataIter++) { CIccCfgDataEntry* pData = dataIter->get(); @@ -531,6 +613,7 @@ int main(int argc, const char* argv[]) outData.m_data.push_back(out); } + } //Now output the data // cfgApply.m_dstType = icCfgIt8; diff --git a/docs/icc-cmm-threading.md b/docs/icc-cmm-threading.md index 5325ac34a..714eaf2b2 100644 --- a/docs/icc-cmm-threading.md +++ b/docs/icc-cmm-threading.md @@ -109,6 +109,10 @@ iccApplyProfiles -threads N -cfg config.json The flag is parsed before `-cfg`, so it must come first. +`iccApplySearch` accepts the same leading option for inverse-search batches; +explicit values are limited to `CIccThreadedCmm::GetMaxThreads()`, and +`-debugcalc` requires `-threads 1`. + ## TIFF Performance Benchmark Use the cross-platform benchmark helper to compare `-threads 1`, `2`, and `0` @@ -142,9 +146,9 @@ undefined. ## Use From IccConnect When a CMM is built through [`CIccConnectCmm`](icc-connect.md), pass -`nThreads` into `CreateStandard` directly — the factory installs the -wrapper internally and `CIccConnectCmm` keeps single ownership of the -whole stack: +`nThreads` into `CreateStandard` or the three-argument `CreateSearch`; the +factory installs the wrapper internally and `CIccConnectCmm` keeps single +ownership of the whole stack: ```cpp std::string sError; @@ -164,14 +168,13 @@ delete conn; // tears down wrapper + underlying `conn->IsThreaded()` reports whether the wrapper was installed; `conn->GetThreadedCmm()` returns the wrapper directly when needed. The -type-specific accessors `GetNamedCmm()` and `GetSearchCmm()` return null -once the threaded wrapper is in place — striped apply only fits the -standard pixel-pipeline use case. +`GetNamedCmm()` accessor returns null through a threaded wrapper; +`GetSearchCmm()` unwraps a threaded search CMM. ## See Also - [IccConnect library](icc-connect.md) — factory that constructs a `Begin()`-ed CMM from JSON config, with an `nThreads` parameter on - `CreateStandard` for parallel apply. + `CreateStandard` and `CreateSearch` for parallel apply. - [CLI tool reference](tools-cli-reference.md) — shared option tables for the `iccApply*` tools. diff --git a/docs/icc-connect.md b/docs/icc-connect.md index 07a6f8155..c47b2a8f9 100644 --- a/docs/icc-connect.md +++ b/docs/icc-connect.md @@ -46,6 +46,7 @@ factory methods that load profiles, register hints, attach PCCs, and call | `CreateStandard(profiles, embeddedData=null, embeddedLen=0, nThreads=1, pErrorMsg=nullptr)` | `CIccCfgProfileSequence` | Standard pixel-pipeline CMM; if `embeddedData` is supplied and the first profile config has an empty `iccFile`, that buffer is loaded as the first xform (e.g. for ICC profiles embedded in TIFF/JPEG). When `nThreads != 1`, the returned wrapper is a `CIccThreadedCmm` over the underlying CMM. | | `CreateNamed(profiles, srcSpace=icSigUnknownData, bInputProfile=true, pErrorMsg=nullptr)` | `CIccCfgProfileSequence` | Named-color CMM (`CIccNamedColorCmm`) for named-color workflows. | | `CreateSearch(searchApply, pErrorMsg=nullptr)` | `CIccCfgSearchApply` | Spectral inverse-search CMM (`CIccCmmSearch`) with weighted PCC attach and optional destination init profile. | +| `CreateSearch(searchApply, pErrorMsg, nThreads)` | `CIccCfgSearchApply` | Threaded inverse search; `0` selects hardware concurrency, `1` is scalar, and `2` through `GetMaxThreads()` select workers. | | `Attach(pCmm)` | Any `CIccCmm*` | Wraps an externally constructed CMM; the wrapper takes ownership. | All factories return `nullptr` on any failure (profile load, hint allocation, @@ -56,7 +57,7 @@ passed to `CreateStandard` are not retained by the library. ### Error Reporting The library never writes to `stderr`. To learn *why* a factory call -returned `nullptr`, pass a `std::string*` as the final `pErrorMsg` +returned `nullptr`, pass a `std::string*` through the `pErrorMsg` argument. On failure it is populated with a single human-readable line describing the first failure encountered, prefixed where applicable with the failing stage index. Examples: @@ -78,8 +79,8 @@ discard it. Passing `nullptr` (the default) silently drops the message. ```cpp CIccCmm* GetCmm() const; // wrapped CMM (never null after success) CIccNamedColorCmm* GetNamedCmm() const; // non-null only for CreateNamed -CIccCmmSearch* GetSearchCmm() const; // non-null only for CreateSearch -CIccThreadedCmm* GetThreadedCmm() const; // non-null when CreateStandard wrapped with nThreads != 1 +CIccCmmSearch* GetSearchCmm() const; // non-null for scalar or threaded CreateSearch +CIccThreadedCmm* GetThreadedCmm() const; // non-null when CreateStandard or CreateSearch wraps bool IsThreaded() const; // shorthand for GetThreadedCmm() != nullptr icColorSpaceSignature GetSourceSpace() const; icColorSpaceSignature GetDestSpace() const; @@ -91,16 +92,16 @@ object and free with `delete`. ### Parallel Apply Threading is selected at construction time via the `nThreads` argument to -`CreateStandard`. When `nThreads == 0` the factory uses +`CreateStandard` or the three-argument `CreateSearch`. When `nThreads == 0` +the factory uses `std::thread::hardware_concurrency()`; when `nThreads > 1` it uses that many workers; when `nThreads == 1` the underlying CMM is returned without the threaded wrapper. See the [threading guide](icc-cmm-threading.md) for the apply-time semantics. -Because the threaded wrapper hides the type-specific CMM, -`GetNamedCmm()` and `GetSearchCmm()` return `nullptr` once a threaded -wrapper is in place. `GetThreadedCmm()` and `IsThreaded()` expose the -wrapper directly when needed. +A threaded wrapper hides a named-color CMM, so `GetNamedCmm()` returns +`nullptr`. `GetSearchCmm()` unwraps a threaded search CMM; +`GetThreadedCmm()` and `IsThreaded()` expose the wrapper directly. For a worked example, see [`Tools/CmdLine/IccApplyProfiles/iccApplyProfiles.cpp`](../Tools/CmdLine/IccApplyProfiles/iccApplyProfiles.cpp), diff --git a/docs/tools-cli-reference.md b/docs/tools-cli-reference.md index d8be0bb33..99fb79fe0 100644 --- a/docs/tools-cli-reference.md +++ b/docs/tools-cli-reference.md @@ -27,6 +27,10 @@ single index for common command shapes and shared option tables. | `iccApplyToLink` | Build DeviceLink profiles or `.cube` LUTs | `iccApplyToLink output.icc 0 33 1 "Link" 0.0 1.0 1 1 src.icc 1 dst.icc 1` | | `iccRoundTrip` | Evaluate round-trip behavior | `iccRoundTrip profile.icc` | +`iccApplySearch` accepts an optional leading `-threads N`. `N=0` uses hardware +concurrency, `N=1` is scalar, and larger values select workers up to +`CIccThreadedCmm::GetMaxThreads()`; `-debugcalc` requires `N=1`. + For `iccApplyToLink`, `link_type=0` writes an ICC DeviceLink and `option` selects profile version (`0` for v4, `1` for v5). `link_type=1` writes a `.cube` text LUT and `option` is the precision (`0` through `20`). Other