LCOV - code coverage report
Current view: top level - frmts/zarr - zarr_v3_array.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 1559 1806 86.3 %
Date: 2026-03-11 12:18:57 Functions: 49 51 96.1 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  GDAL
       4             :  * Purpose:  Zarr driver
       5             :  * Author:   Even Rouault <even dot rouault at spatialys.com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2021, Even Rouault <even dot rouault at spatialys.com>
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #include "cpl_enumerate.h"
      14             : #include "cpl_float.h"
      15             : #include "cpl_vsi_virtual.h"
      16             : #include "cpl_worker_thread_pool.h"
      17             : #include "gdal_thread_pool.h"
      18             : #include "zarr.h"
      19             : #include "zarr_v3_codec.h"
      20             : 
      21             : #include <algorithm>
      22             : #include <atomic>
      23             : #include <cassert>
      24             : #include <cinttypes>
      25             : #include <cmath>
      26             : #include <cstdlib>
      27             : #include <limits>
      28             : #include <map>
      29             : #include <set>
      30             : 
      31             : /************************************************************************/
      32             : /*                      ZarrV3Array::ZarrV3Array()                      */
      33             : /************************************************************************/
      34             : 
      35        1324 : ZarrV3Array::ZarrV3Array(
      36             :     const std::shared_ptr<ZarrSharedResource> &poSharedResource,
      37             :     const std::shared_ptr<ZarrGroupBase> &poParent, const std::string &osName,
      38             :     const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
      39             :     const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
      40             :     const std::vector<GUInt64> &anOuterBlockSize,
      41        1324 :     const std::vector<GUInt64> &anInnerBlockSize)
      42        1324 :     : GDALAbstractMDArray(poParent->GetFullName(), osName),
      43             :       ZarrArray(poSharedResource, poParent, osName, aoDims, oType, aoDtypeElts,
      44        1324 :                 anOuterBlockSize, anInnerBlockSize)
      45             : {
      46        1324 : }
      47             : 
      48             : /************************************************************************/
      49             : /*                        ZarrV3Array::Create()                         */
      50             : /************************************************************************/
      51             : 
      52        1324 : std::shared_ptr<ZarrV3Array> ZarrV3Array::Create(
      53             :     const std::shared_ptr<ZarrSharedResource> &poSharedResource,
      54             :     const std::shared_ptr<ZarrGroupBase> &poParent, const std::string &osName,
      55             :     const std::vector<std::shared_ptr<GDALDimension>> &aoDims,
      56             :     const GDALExtendedDataType &oType, const std::vector<DtypeElt> &aoDtypeElts,
      57             :     const std::vector<GUInt64> &anOuterBlockSize,
      58             :     const std::vector<GUInt64> &anInnerBlockSize)
      59             : {
      60             :     auto arr = std::shared_ptr<ZarrV3Array>(
      61             :         new ZarrV3Array(poSharedResource, poParent, osName, aoDims, oType,
      62        2648 :                         aoDtypeElts, anOuterBlockSize, anInnerBlockSize));
      63        1324 :     if (arr->m_nTotalInnerChunkCount == 0)
      64           1 :         return nullptr;
      65        1323 :     arr->SetSelf(arr);
      66             : 
      67        1323 :     return arr;
      68             : }
      69             : 
      70             : /************************************************************************/
      71             : /*                            ~ZarrV3Array()                            */
      72             : /************************************************************************/
      73             : 
      74        2648 : ZarrV3Array::~ZarrV3Array()
      75             : {
      76        1324 :     ZarrV3Array::Flush();
      77        2648 : }
      78             : 
      79             : /************************************************************************/
      80             : /*                               Flush()                                */
      81             : /************************************************************************/
      82             : 
      83        5896 : bool ZarrV3Array::Flush()
      84             : {
      85        5896 :     if (!m_bValid)
      86          10 :         return true;
      87             : 
      88             :     // Flush last dirty block (may add to shard write cache)
      89        5886 :     bool ret = ZarrV3Array::FlushDirtyBlock();
      90             : 
      91             :     // Encode and write all cached shards
      92        5886 :     if (!ZarrV3Array::FlushShardCache())
      93           0 :         ret = false;
      94             : 
      95        5886 :     m_anCachedBlockIndices.clear();
      96             : 
      97        5886 :     if (!m_aoDims.empty())
      98             :     {
      99       10822 :         for (const auto &poDim : m_aoDims)
     100             :         {
     101             :             const auto poZarrDim =
     102        7941 :                 dynamic_cast<const ZarrDimension *>(poDim.get());
     103        7941 :             if (poZarrDim && poZarrDim->IsXArrayDimension())
     104             :             {
     105        5108 :                 if (poZarrDim->IsModified())
     106           8 :                     m_bDefinitionModified = true;
     107             :             }
     108             :             else
     109             :             {
     110        2833 :                 break;
     111             :             }
     112             :         }
     113             :     }
     114             : 
     115        5886 :     CPLJSONObject oAttrs;
     116       11720 :     if (m_oAttrGroup.IsModified() || m_bUnitModified || m_bOffsetModified ||
     117       11720 :         m_bScaleModified || m_bSRSModified)
     118             :     {
     119          52 :         m_bNew = false;
     120             : 
     121          52 :         oAttrs = SerializeSpecialAttributes();
     122             : 
     123          52 :         m_bDefinitionModified = true;
     124             :     }
     125             : 
     126        5886 :     if (m_bDefinitionModified)
     127             :     {
     128         234 :         if (!Serialize(oAttrs))
     129           1 :             ret = false;
     130         234 :         m_bDefinitionModified = false;
     131             :     }
     132             : 
     133        5886 :     return ret;
     134             : }
     135             : 
     136             : /************************************************************************/
     137             : /*                       ZarrV3Array::Serialize()                       */
     138             : /************************************************************************/
     139             : 
     140         234 : bool ZarrV3Array::Serialize(const CPLJSONObject &oAttrs)
     141             : {
     142         468 :     CPLJSONDocument oDoc;
     143         468 :     CPLJSONObject oRoot = oDoc.GetRoot();
     144             : 
     145         234 :     oRoot.Add("zarr_format", 3);
     146         234 :     oRoot.Add("node_type", "array");
     147             : 
     148         234 :     CPLJSONArray oShape;
     149         621 :     for (const auto &poDim : m_aoDims)
     150             :     {
     151         387 :         oShape.Add(static_cast<GInt64>(poDim->GetSize()));
     152             :     }
     153         234 :     oRoot.Add("shape", oShape);
     154             : 
     155         234 :     oRoot.Add("data_type", m_dtype.ToString());
     156             : 
     157             :     {
     158         468 :         CPLJSONObject oChunkGrid;
     159         234 :         oRoot.Add("chunk_grid", oChunkGrid);
     160         234 :         oChunkGrid.Add("name", "regular");
     161         468 :         CPLJSONObject oConfiguration;
     162         234 :         oChunkGrid.Add("configuration", oConfiguration);
     163         234 :         CPLJSONArray oChunks;
     164         621 :         for (const auto nBlockSize : m_anOuterBlockSize)
     165             :         {
     166         387 :             oChunks.Add(static_cast<GInt64>(nBlockSize));
     167             :         }
     168         234 :         oConfiguration.Add("chunk_shape", oChunks);
     169             :     }
     170             : 
     171             :     {
     172         468 :         CPLJSONObject oChunkKeyEncoding;
     173         234 :         oRoot.Add("chunk_key_encoding", oChunkKeyEncoding);
     174         234 :         oChunkKeyEncoding.Add("name", m_bV2ChunkKeyEncoding ? "v2" : "default");
     175         234 :         CPLJSONObject oConfiguration;
     176         234 :         oChunkKeyEncoding.Add("configuration", oConfiguration);
     177         234 :         oConfiguration.Add("separator", m_osDimSeparator);
     178             :     }
     179             : 
     180         234 :     if (m_oType.GetClass() == GEDTC_STRING && m_pabyNoData != nullptr)
     181             :     {
     182           1 :         char *pStr = nullptr;
     183           1 :         memcpy(&pStr, m_pabyNoData, sizeof(pStr));
     184           1 :         oRoot.Add("fill_value", pStr ? pStr : "");
     185             :     }
     186         233 :     else if (m_pabyNoData == nullptr)
     187             :     {
     188         405 :         if (m_oType.GetNumericDataType() == GDT_Float16 ||
     189         405 :             m_oType.GetNumericDataType() == GDT_Float32 ||
     190         169 :             m_oType.GetNumericDataType() == GDT_Float64)
     191             :         {
     192          63 :             oRoot.Add("fill_value", "NaN");
     193             :         }
     194             :         else
     195             :         {
     196         140 :             oRoot.AddNull("fill_value");
     197             :         }
     198             :     }
     199             :     else
     200             :     {
     201          60 :         if (m_oType.GetNumericDataType() == GDT_CFloat16 ||
     202          60 :             m_oType.GetNumericDataType() == GDT_CFloat32 ||
     203          22 :             m_oType.GetNumericDataType() == GDT_CFloat64)
     204             :         {
     205             :             double adfNoDataValue[2];
     206          16 :             GDALCopyWords(m_pabyNoData, m_oType.GetNumericDataType(), 0,
     207             :                           adfNoDataValue, GDT_CFloat64, 0, 1);
     208          16 :             CPLJSONArray oArray;
     209          48 :             for (int i = 0; i < 2; ++i)
     210             :             {
     211          32 :                 if (std::isnan(adfNoDataValue[i]))
     212           6 :                     oArray.Add("NaN");
     213          26 :                 else if (adfNoDataValue[i] ==
     214          26 :                          std::numeric_limits<double>::infinity())
     215           4 :                     oArray.Add("Infinity");
     216          44 :                 else if (adfNoDataValue[i] ==
     217          22 :                          -std::numeric_limits<double>::infinity())
     218           4 :                     oArray.Add("-Infinity");
     219             :                 else
     220          18 :                     oArray.Add(adfNoDataValue[i]);
     221             :             }
     222          16 :             oRoot.Add("fill_value", oArray);
     223             :         }
     224             :         else
     225             :         {
     226          14 :             SerializeNumericNoData(oRoot);
     227             :         }
     228             :     }
     229             : 
     230         234 :     if (m_poCodecs)
     231             :     {
     232         218 :         oRoot.Add("codecs", m_poCodecs->GetJSon());
     233             :     }
     234             : 
     235         234 :     oRoot.Add("attributes", oAttrs);
     236             : 
     237             :     // Set dimension_names
     238         234 :     if (!m_aoDims.empty())
     239             :     {
     240         430 :         CPLJSONArray oDimensions;
     241         586 :         for (const auto &poDim : m_aoDims)
     242             :         {
     243             :             const auto poZarrDim =
     244         387 :                 dynamic_cast<const ZarrDimension *>(poDim.get());
     245         387 :             if (poZarrDim && poZarrDim->IsXArrayDimension())
     246             :             {
     247         371 :                 oDimensions.Add(poDim->GetName());
     248             :             }
     249             :             else
     250             :             {
     251          16 :                 oDimensions = CPLJSONArray();
     252          16 :                 break;
     253             :             }
     254             :         }
     255         215 :         if (oDimensions.Size() > 0)
     256             :         {
     257         199 :             oRoot.Add("dimension_names", oDimensions);
     258             :         }
     259             :     }
     260             : 
     261             :     // TODO: codecs
     262             : 
     263         234 :     const bool bRet = oDoc.Save(m_osFilename);
     264         234 :     if (bRet)
     265         233 :         m_poSharedResource->SetZMetadataItem(m_osFilename, oRoot);
     266         468 :     return bRet;
     267             : }
     268             : 
     269             : /************************************************************************/
     270             : /*                   ZarrV3Array::NeedDecodedBuffer()                   */
     271             : /************************************************************************/
     272             : 
     273       17199 : bool ZarrV3Array::NeedDecodedBuffer() const
     274             : {
     275       34398 :     for (const auto &elt : m_aoDtypeElts)
     276             :     {
     277       17199 :         if (elt.needByteSwapping)
     278             :         {
     279           0 :             return true;
     280             :         }
     281             :     }
     282       17199 :     return false;
     283             : }
     284             : 
     285             : /************************************************************************/
     286             : /*                ZarrV3Array::AllocateWorkingBuffers()                 */
     287             : /************************************************************************/
     288             : 
     289         993 : bool ZarrV3Array::AllocateWorkingBuffers() const
     290             : {
     291         993 :     if (m_bAllocateWorkingBuffersDone)
     292          77 :         return m_bWorkingBuffersOK;
     293             : 
     294         916 :     m_bAllocateWorkingBuffersDone = true;
     295             : 
     296         916 :     size_t nSizeNeeded = m_nInnerBlockSizeBytes;
     297         916 :     if (NeedDecodedBuffer())
     298             :     {
     299           0 :         size_t nDecodedBufferSize = m_oType.GetSize();
     300           0 :         for (const auto &nBlockSize : m_anInnerBlockSize)
     301             :         {
     302           0 :             if (nDecodedBufferSize > std::numeric_limits<size_t>::max() /
     303           0 :                                          static_cast<size_t>(nBlockSize))
     304             :             {
     305           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     306           0 :                 return false;
     307             :             }
     308           0 :             nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
     309             :         }
     310           0 :         if (nSizeNeeded >
     311           0 :             std::numeric_limits<size_t>::max() - nDecodedBufferSize)
     312             :         {
     313           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Too large chunk size");
     314           0 :             return false;
     315             :         }
     316           0 :         nSizeNeeded += nDecodedBufferSize;
     317             :     }
     318             : 
     319             :     // Reserve a buffer for tile content
     320         916 :     if (nSizeNeeded > 1024 * 1024 * 1024 &&
     321           0 :         !CPLTestBool(CPLGetConfigOption("ZARR_ALLOW_BIG_TILE_SIZE", "NO")))
     322             :     {
     323           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     324             :                  "Zarr tile allocation would require " CPL_FRMT_GUIB " bytes. "
     325             :                  "By default the driver limits to 1 GB. To allow that memory "
     326             :                  "allocation, set the ZARR_ALLOW_BIG_TILE_SIZE configuration "
     327             :                  "option to YES.",
     328             :                  static_cast<GUIntBig>(nSizeNeeded));
     329           0 :         return false;
     330             :     }
     331             : 
     332         916 :     m_bWorkingBuffersOK =
     333         916 :         AllocateWorkingBuffers(m_abyRawBlockData, m_abyDecodedBlockData);
     334         916 :     return m_bWorkingBuffersOK;
     335             : }
     336             : 
     337       15768 : bool ZarrV3Array::AllocateWorkingBuffers(
     338             :     ZarrByteVectorQuickResize &abyRawBlockData,
     339             :     ZarrByteVectorQuickResize &abyDecodedBlockData) const
     340             : {
     341             :     // This method should NOT modify any ZarrArray member, as it is going to
     342             :     // be called concurrently from several threads.
     343             : 
     344             :     // Set those #define to avoid accidental use of some global variables
     345             : #define m_abyRawBlockData cannot_use_here
     346             : #define m_abyDecodedBlockData cannot_use_here
     347             : 
     348       15768 :     const size_t nSizeNeeded = m_nInnerBlockSizeBytes;
     349             :     try
     350             :     {
     351       15768 :         abyRawBlockData.resize(nSizeNeeded);
     352             :     }
     353           0 :     catch (const std::bad_alloc &e)
     354             :     {
     355           0 :         CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
     356           0 :         return false;
     357             :     }
     358             : 
     359       15768 :     if (NeedDecodedBuffer())
     360             :     {
     361           0 :         size_t nDecodedBufferSize = m_oType.GetSize();
     362           0 :         for (const auto &nBlockSize : m_anInnerBlockSize)
     363             :         {
     364           0 :             nDecodedBufferSize *= static_cast<size_t>(nBlockSize);
     365             :         }
     366             :         try
     367             :         {
     368           0 :             abyDecodedBlockData.resize(nDecodedBufferSize);
     369             :         }
     370           0 :         catch (const std::bad_alloc &e)
     371             :         {
     372           0 :             CPLError(CE_Failure, CPLE_OutOfMemory, "%s", e.what());
     373           0 :             return false;
     374             :         }
     375             :     }
     376             : 
     377       15768 :     return true;
     378             : #undef m_abyRawBlockData
     379             : #undef m_abyDecodedBlockData
     380             : }
     381             : 
     382             : /************************************************************************/
     383             : /*                     ZarrV3Array::LoadBlockData()                     */
     384             : /************************************************************************/
     385             : 
     386        1793 : bool ZarrV3Array::LoadBlockData(const uint64_t *blockIndices,
     387             :                                 bool &bMissingBlockOut) const
     388             : {
     389        1793 :     return LoadBlockData(blockIndices,
     390             :                          false,  // use mutex
     391        1793 :                          m_poCodecs.get(), m_abyRawBlockData,
     392        3586 :                          m_abyDecodedBlockData, bMissingBlockOut);
     393             : }
     394             : 
     395       16645 : bool ZarrV3Array::LoadBlockData(const uint64_t *blockIndices, bool bUseMutex,
     396             :                                 ZarrV3CodecSequence *poCodecs,
     397             :                                 ZarrByteVectorQuickResize &abyRawBlockData,
     398             :                                 ZarrByteVectorQuickResize &abyDecodedBlockData,
     399             :                                 bool &bMissingBlockOut) const
     400             : {
     401             :     // This method should NOT modify any ZarrArray member, as it is going to
     402             :     // be called concurrently from several threads.
     403             : 
     404             :     // Set those #define to avoid accidental use of some global variables
     405             : #define m_abyRawBlockData cannot_use_here
     406             : #define m_abyDecodedBlockData cannot_use_here
     407             : #define m_poCodecs cannot_use_here
     408             : 
     409       16645 :     bMissingBlockOut = false;
     410             : 
     411       33290 :     std::string osFilename;
     412       16645 :     if (poCodecs && poCodecs->SupportsPartialDecoding())
     413             :     {
     414        1050 :         std::vector<uint64_t> outerChunkIndices;
     415        3156 :         for (size_t i = 0; i < GetDimensionCount(); ++i)
     416             :         {
     417             :             // Note: m_anOuterBlockSize[i]/m_anInnerBlockSize[i] is an integer
     418        6318 :             outerChunkIndices.push_back(blockIndices[i] *
     419        4212 :                                         m_anInnerBlockSize[i] /
     420        2106 :                                         m_anOuterBlockSize[i]);
     421             :         }
     422             : 
     423        1050 :         osFilename = BuildChunkFilename(outerChunkIndices.data());
     424             :     }
     425             :     else
     426             :     {
     427       15595 :         osFilename = BuildChunkFilename(blockIndices);
     428             :     }
     429             : 
     430             :     // For network file systems, get the streaming version of the filename,
     431             :     // as we don't need arbitrary seeking in the file
     432             :     // ... unless we do partial decoding, in which case range requests within
     433             :     // a shard are much more efficient
     434       16645 :     if (!(poCodecs && poCodecs->SupportsPartialDecoding()))
     435             :     {
     436       15595 :         osFilename = VSIFileManager::GetHandler(osFilename.c_str())
     437       15595 :                          ->GetStreamingFilename(osFilename);
     438             :     }
     439             : 
     440             :     // First if we have a tile presence cache, check tile presence from it
     441             :     bool bEarlyRet;
     442       16645 :     if (bUseMutex)
     443             :     {
     444       14852 :         std::lock_guard<std::mutex> oLock(m_oMutex);
     445       14852 :         bEarlyRet = IsBlockMissingFromCacheInfo(osFilename, blockIndices);
     446             :     }
     447             :     else
     448             :     {
     449        1793 :         bEarlyRet = IsBlockMissingFromCacheInfo(osFilename, blockIndices);
     450             :     }
     451       16645 :     if (bEarlyRet)
     452             :     {
     453          26 :         bMissingBlockOut = true;
     454          26 :         return true;
     455             :     }
     456       16619 :     VSIVirtualHandleUniquePtr fp;
     457             :     // This is the number of files returned in a S3 directory listing operation
     458       16619 :     constexpr uint64_t MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING = 1000;
     459       16619 :     const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
     460             :                                            nullptr};
     461       16619 :     const auto nErrorBefore = CPLGetErrorCounter();
     462       33229 :     if ((m_osDimSeparator == "/" && !m_anOuterBlockSize.empty() &&
     463       49857 :          m_anOuterBlockSize.back() > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING) ||
     464       16619 :         (m_osDimSeparator != "/" &&
     465           9 :          m_nTotalInnerChunkCount > MAX_TILES_ALLOWED_FOR_DIRECTORY_LISTING))
     466             :     {
     467             :         // Avoid issuing ReadDir() when a lot of files are expected
     468             :         CPLConfigOptionSetter optionSetter("GDAL_DISABLE_READDIR_ON_OPEN",
     469           0 :                                            "YES", true);
     470           0 :         fp.reset(VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
     471             :     }
     472             :     else
     473             :     {
     474       16619 :         fp.reset(VSIFOpenEx2L(osFilename.c_str(), "rb", 0, apszOpenOptions));
     475             :     }
     476       16619 :     if (fp == nullptr)
     477             :     {
     478         349 :         if (nErrorBefore != CPLGetErrorCounter())
     479             :         {
     480           0 :             return false;
     481             :         }
     482             :         else
     483             :         {
     484             :             // Missing files are OK and indicate nodata_value
     485         349 :             CPLDebugOnly(ZARR_DEBUG_KEY, "Block %s missing (=nodata)",
     486             :                          osFilename.c_str());
     487         349 :             bMissingBlockOut = true;
     488         349 :             return true;
     489             :         }
     490             :     }
     491             : 
     492       16270 :     bMissingBlockOut = false;
     493             : 
     494       16270 :     if (poCodecs && poCodecs->SupportsPartialDecoding())
     495             :     {
     496        1003 :         std::vector<size_t> anStartIdx;
     497        1003 :         std::vector<size_t> anCount;
     498        3009 :         for (size_t i = 0; i < GetDimensionCount(); ++i)
     499             :         {
     500        2006 :             anStartIdx.push_back(
     501        4012 :                 static_cast<size_t>((blockIndices[i] * m_anInnerBlockSize[i]) %
     502        2006 :                                     m_anOuterBlockSize[i]));
     503        2006 :             anCount.push_back(static_cast<size_t>(m_anInnerBlockSize[i]));
     504             :         }
     505        1003 :         if (!poCodecs->DecodePartial(fp.get(), abyRawBlockData, anStartIdx,
     506             :                                      anCount))
     507         356 :             return false;
     508             :     }
     509             :     else
     510             :     {
     511       15267 :         CPLAssert(abyRawBlockData.capacity() >= m_nInnerBlockSizeBytes);
     512             :         // should not fail
     513       15267 :         abyRawBlockData.resize(m_nInnerBlockSizeBytes);
     514             : 
     515       15267 :         bool bRet = true;
     516       15267 :         size_t nRawDataSize = abyRawBlockData.size();
     517       15267 :         if (poCodecs == nullptr)
     518             :         {
     519           6 :             nRawDataSize = fp->Read(&abyRawBlockData[0], 1, nRawDataSize);
     520             :         }
     521             :         else
     522             :         {
     523       15261 :             fp->Seek(0, SEEK_END);
     524       15261 :             const auto nSize = fp->Tell();
     525       15261 :             fp->Seek(0, SEEK_SET);
     526       15261 :             if (nSize >
     527       15261 :                 static_cast<vsi_l_offset>(std::numeric_limits<int>::max()))
     528             :             {
     529           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Too large tile %s",
     530             :                          osFilename.c_str());
     531           0 :                 bRet = false;
     532             :             }
     533             :             else
     534             :             {
     535             :                 try
     536             :                 {
     537       15261 :                     abyRawBlockData.resize(static_cast<size_t>(nSize));
     538             :                 }
     539           0 :                 catch (const std::exception &)
     540             :                 {
     541           0 :                     CPLError(CE_Failure, CPLE_OutOfMemory,
     542             :                              "Cannot allocate memory for tile %s",
     543             :                              osFilename.c_str());
     544           0 :                     bRet = false;
     545             :                 }
     546             : 
     547       45782 :                 if (bRet &&
     548       30521 :                     (abyRawBlockData.empty() ||
     549       15260 :                      fp->Read(&abyRawBlockData[0], 1, abyRawBlockData.size()) !=
     550       15260 :                          abyRawBlockData.size()))
     551             :                 {
     552           1 :                     CPLError(CE_Failure, CPLE_AppDefined,
     553             :                              "Could not read tile %s correctly",
     554             :                              osFilename.c_str());
     555           1 :                     bRet = false;
     556             :                 }
     557             :                 else
     558             :                 {
     559       15260 :                     if (!poCodecs->Decode(abyRawBlockData))
     560             :                     {
     561         131 :                         CPLError(CE_Failure, CPLE_AppDefined,
     562             :                                  "Decompression of tile %s failed",
     563             :                                  osFilename.c_str());
     564         131 :                         bRet = false;
     565             :                     }
     566             :                 }
     567             :             }
     568             :         }
     569       15267 :         if (!bRet)
     570         132 :             return false;
     571             : 
     572       15135 :         if (nRawDataSize != abyRawBlockData.size())
     573             :         {
     574           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     575             :                      "Decompressed tile %s has not expected size. "
     576             :                      "Got %u instead of %u",
     577             :                      osFilename.c_str(),
     578           0 :                      static_cast<unsigned>(abyRawBlockData.size()),
     579             :                      static_cast<unsigned>(nRawDataSize));
     580           0 :             return false;
     581             :         }
     582             :     }
     583             : 
     584       15782 :     if (!abyDecodedBlockData.empty())
     585             :     {
     586             :         const size_t nSourceSize =
     587           0 :             m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
     588           0 :         const auto nDTSize = m_oType.GetSize();
     589           0 :         const size_t nValues = abyDecodedBlockData.size() / nDTSize;
     590           0 :         CPLAssert(nValues == m_nInnerBlockSizeBytes / nSourceSize);
     591           0 :         const GByte *pSrc = abyRawBlockData.data();
     592           0 :         GByte *pDst = &abyDecodedBlockData[0];
     593           0 :         for (size_t i = 0; i < nValues;
     594           0 :              i++, pSrc += nSourceSize, pDst += nDTSize)
     595             :         {
     596           0 :             DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
     597             :         }
     598             :     }
     599             : 
     600       15782 :     return true;
     601             : 
     602             : #undef m_abyRawBlockData
     603             : #undef m_abyDecodedBlockData
     604             : #undef m_poCodecs
     605             : }
     606             : 
     607             : /************************************************************************/
     608             : /*                         ZarrV3Array::IRead()                         */
     609             : /************************************************************************/
     610             : 
     611         856 : bool ZarrV3Array::IRead(const GUInt64 *arrayStartIdx, const size_t *count,
     612             :                         const GInt64 *arrayStep, const GPtrDiff_t *bufferStride,
     613             :                         const GDALExtendedDataType &bufferDataType,
     614             :                         void *pDstBuffer) const
     615             : {
     616             :     // For sharded arrays, pre-populate the block cache via ReadMultiRange()
     617             :     // so that the base-class block-by-block loop hits memory, not HTTP.
     618         856 :     if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
     619             :     {
     620         552 :         PreloadShardedBlocks(arrayStartIdx, count);
     621             :     }
     622         856 :     return ZarrArray::IRead(arrayStartIdx, count, arrayStep, bufferStride,
     623         856 :                             bufferDataType, pDstBuffer);
     624             : }
     625             : 
     626             : /************************************************************************/
     627             : /*                 ZarrV3Array::PreloadShardedBlocks()                  */
     628             : /************************************************************************/
     629             : 
     630         556 : void ZarrV3Array::PreloadShardedBlocks(const GUInt64 *arrayStartIdx,
     631             :                                        const size_t *count) const
     632             : {
     633         556 :     const size_t nDims = m_aoDims.size();
     634         556 :     if (nDims == 0)
     635         555 :         return;
     636             : 
     637             :     // Calculate needed block index range
     638        1112 :     std::vector<uint64_t> anBlockMin(nDims), anBlockMax(nDims);
     639         556 :     size_t nTotalBlocks = 1;
     640        1672 :     for (size_t i = 0; i < nDims; ++i)
     641             :     {
     642        1116 :         anBlockMin[i] = arrayStartIdx[i] / m_anInnerBlockSize[i];
     643        2232 :         anBlockMax[i] =
     644        1116 :             (arrayStartIdx[i] + count[i] - 1) / m_anInnerBlockSize[i];
     645        1116 :         nTotalBlocks *= static_cast<size_t>(anBlockMax[i] - anBlockMin[i] + 1);
     646             :     }
     647             : 
     648         556 :     if (nTotalBlocks <= 1)
     649          33 :         return;  // single block — no batching benefit
     650             : 
     651         523 :     CPLDebugOnly("ZARR", "PreloadShardedBlocks: %" PRIu64 " blocks to batch",
     652             :                  static_cast<uint64_t>(nTotalBlocks));
     653             : 
     654             :     // Enumerate all needed blocks, grouped by shard filename
     655             :     struct BlockInfo
     656             :     {
     657             :         std::vector<uint64_t> anBlockIndices{};
     658             :         std::vector<size_t> anStartIdx{};
     659             :         std::vector<size_t> anCount{};
     660             :     };
     661             : 
     662         523 :     std::map<std::string, std::vector<BlockInfo>> oShardToBlocks;
     663             : 
     664             :     // Iterate over all needed block indices
     665         523 :     std::vector<uint64_t> anCur(nDims);
     666         523 :     size_t dimIdx = 0;
     667       15915 : lbl_next:
     668       15915 :     if (dimIdx == nDims)
     669             :     {
     670             :         // Skip blocks already in cache
     671       25610 :         const std::vector<uint64_t> cacheKey(anCur.begin(), anCur.end());
     672       12805 :         if (m_oChunkCache.find(cacheKey) == m_oChunkCache.end())
     673             :         {
     674             :             // Compute shard filename and inner chunk start/count
     675       25274 :             std::vector<uint64_t> outerIdx(nDims);
     676       25274 :             BlockInfo info;
     677       12637 :             info.anBlockIndices = anCur;
     678       12637 :             info.anStartIdx.resize(nDims);
     679       12637 :             info.anCount.resize(nDims);
     680       37957 :             for (size_t i = 0; i < nDims; ++i)
     681             :             {
     682       50640 :                 outerIdx[i] =
     683       25320 :                     anCur[i] * m_anInnerBlockSize[i] / m_anOuterBlockSize[i];
     684       50640 :                 info.anStartIdx[i] = static_cast<size_t>(
     685       25320 :                     (anCur[i] * m_anInnerBlockSize[i]) % m_anOuterBlockSize[i]);
     686       25320 :                 info.anCount[i] = static_cast<size_t>(m_anInnerBlockSize[i]);
     687             :             }
     688             : 
     689       25274 :             std::string osFilename = BuildChunkFilename(outerIdx.data());
     690       12637 :             oShardToBlocks[osFilename].push_back(std::move(info));
     691             :         }
     692             :     }
     693             :     else
     694             :     {
     695        3110 :         anCur[dimIdx] = anBlockMin[dimIdx];
     696             :         while (true)
     697             :         {
     698       15392 :             dimIdx++;
     699       15392 :             goto lbl_next;
     700       15392 :         lbl_return:
     701       15392 :             dimIdx--;
     702       15392 :             if (anCur[dimIdx] == anBlockMax[dimIdx])
     703        3110 :                 break;
     704       12282 :             ++anCur[dimIdx];
     705             :         }
     706             :     }
     707       15915 :     if (dimIdx > 0)
     708       15392 :         goto lbl_return;
     709             : 
     710             :     // Collect shards that qualify for batching (>1 block)
     711             :     struct ShardWork
     712             :     {
     713             :         const std::string *posFilename;
     714             :         std::vector<BlockInfo> *paBlocks;
     715             :     };
     716             : 
     717         523 :     std::vector<ShardWork> aShardWork;
     718        5065 :     for (auto &[osFilename, aBlocks] : oShardToBlocks)
     719             :     {
     720        4542 :         if (aBlocks.size() > 1)
     721        4035 :             aShardWork.push_back({&osFilename, &aBlocks});
     722             :     }
     723             : 
     724         523 :     if (aShardWork.empty())
     725           8 :         return;
     726             : 
     727         515 :     const char *const apszOpenOptions[] = {"IGNORE_FILENAME_RESTRICTIONS=YES",
     728             :                                            nullptr};
     729         515 :     const bool bNeedDecode = NeedDecodedBuffer();
     730             : 
     731             :     // Process one shard: open file, batch-decode, type-convert, cache.
     732             :     // poCodecs: per-thread clone (parallel) or m_poCodecs (sequential).
     733             :     // oMutex: guards cache writes (uncontended in sequential path).
     734             :     const auto ProcessOneShard =
     735        4035 :         [this, &apszOpenOptions, bNeedDecode](const ShardWork &work,
     736             :                                               ZarrV3CodecSequence *poCodecs,
     737       29094 :                                               std::mutex &oMutex)
     738             :     {
     739             :         VSIVirtualHandleUniquePtr fp(
     740        4035 :             VSIFOpenEx2L(work.posFilename->c_str(), "rb", 0, apszOpenOptions));
     741        4035 :         if (!fp)
     742           4 :             return;
     743             : 
     744        4031 :         const auto &aBlocks = *work.paBlocks;
     745             :         std::vector<std::pair<std::vector<size_t>, std::vector<size_t>>>
     746        4031 :             anRequests;
     747        4031 :         anRequests.reserve(aBlocks.size());
     748       16147 :         for (const auto &info : aBlocks)
     749       12116 :             anRequests.push_back({info.anStartIdx, info.anCount});
     750             : 
     751        4031 :         std::vector<ZarrByteVectorQuickResize> aResults;
     752        4031 :         if (!poCodecs->BatchDecodePartial(fp.get(), work.posFilename->c_str(),
     753             :                                           anRequests, aResults))
     754         356 :             return;
     755             : 
     756             :         // Type-convert outside mutex (CPU-bound, thread-local data)
     757        7350 :         std::vector<ZarrByteVectorQuickResize> aDecoded;
     758        3675 :         if (bNeedDecode)
     759             :         {
     760           0 :             const size_t nSourceSize = m_aoDtypeElts.back().nativeOffset +
     761           0 :                                        m_aoDtypeElts.back().nativeSize;
     762           0 :             const auto nGDALDTSize = m_oType.GetSize();
     763           0 :             aDecoded.resize(aBlocks.size());
     764           0 :             for (size_t i = 0; i < aBlocks.size(); ++i)
     765             :             {
     766           0 :                 if (aResults[i].empty())
     767           0 :                     continue;
     768           0 :                 const size_t nValues = aResults[i].size() / nSourceSize;
     769           0 :                 aDecoded[i].resize(nValues * nGDALDTSize);
     770           0 :                 const GByte *pSrc = aResults[i].data();
     771           0 :                 GByte *pDst = aDecoded[i].data();
     772           0 :                 for (size_t v = 0; v < nValues;
     773           0 :                      v++, pSrc += nSourceSize, pDst += nGDALDTSize)
     774             :                 {
     775           0 :                     DecodeSourceElt(m_aoDtypeElts, pSrc, pDst);
     776             :                 }
     777             :             }
     778             :         }
     779             : 
     780             :         // Store in cache under mutex
     781        7350 :         std::lock_guard<std::mutex> oLock(oMutex);
     782       14367 :         for (size_t i = 0; i < aBlocks.size(); ++i)
     783             :         {
     784       21384 :             CachedBlock cachedBlock;
     785       10692 :             if (!aResults[i].empty())
     786             :             {
     787       10692 :                 if (bNeedDecode)
     788           0 :                     std::swap(cachedBlock.abyDecoded, aDecoded[i]);
     789             :                 else
     790       10692 :                     std::swap(cachedBlock.abyDecoded, aResults[i]);
     791             :             }
     792       10692 :             m_oChunkCache[aBlocks[i].anBlockIndices] = std::move(cachedBlock);
     793             :         }
     794         515 :     };
     795             : 
     796         515 :     const int nMaxThreads = GDALGetNumThreads();
     797             : 
     798         515 :     const int nShards = static_cast<int>(aShardWork.size());
     799         515 :     std::mutex oMutex;
     800             : 
     801             :     // Sequential: single thread, single shard, or no thread pool
     802           1 :     CPLWorkerThreadPool *wtp = (nMaxThreads > 1 && nShards > 1)
     803         516 :                                    ? GDALGetGlobalThreadPool(nMaxThreads)
     804         515 :                                    : nullptr;
     805         515 :     if (!wtp)
     806             :     {
     807        4541 :         for (const auto &work : aShardWork)
     808        4027 :             ProcessOneShard(work, m_poCodecs.get(), oMutex);
     809         514 :         return;
     810             :     }
     811             : 
     812           1 :     CPLDebugOnly("ZARR",
     813             :                  "PreloadShardedBlocks: parallel across %d shards (%d threads)",
     814             :                  nShards, std::min(nMaxThreads, nShards));
     815             : 
     816             :     // Clone codecs upfront on main thread (Clone is not thread-safe)
     817           2 :     std::vector<std::unique_ptr<ZarrV3CodecSequence>> apoCodecs(nShards);
     818           9 :     for (int i = 0; i < nShards; ++i)
     819           8 :         apoCodecs[i] = m_poCodecs->Clone();
     820             : 
     821           2 :     auto poQueue = wtp->CreateJobQueue();
     822           9 :     for (int i = 0; i < nShards; ++i)
     823             :     {
     824           8 :         poQueue->SubmitJob([&work = aShardWork[i], pCodecs = apoCodecs[i].get(),
     825           8 :                             &oMutex, &ProcessOneShard]()
     826           8 :                            { ProcessOneShard(work, pCodecs, oMutex); });
     827             :     }
     828           1 :     poQueue->WaitCompletion();
     829             : }
     830             : 
     831             : /************************************************************************/
     832             : /*                      ZarrV3Array::IAdviseRead()                      */
     833             : /************************************************************************/
     834             : 
     835          25 : bool ZarrV3Array::IAdviseRead(const GUInt64 *arrayStartIdx, const size_t *count,
     836             :                               CSLConstList papszOptions) const
     837             : {
     838             :     // For sharded arrays, batch all needed inner chunks via
     839             :     // PreloadShardedBlocks (BatchDecodePartial + ReadMultiRange) instead
     840             :     // of the per-block LoadBlockData path below.
     841          25 :     if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
     842             :     {
     843           4 :         PreloadShardedBlocks(arrayStartIdx, count);
     844           4 :         return true;
     845             :     }
     846             : 
     847          42 :     std::vector<uint64_t> anIndicesCur;
     848          21 :     int nThreadsMax = 0;
     849          42 :     std::vector<uint64_t> anReqBlocksIndices;
     850          21 :     size_t nReqBlocks = 0;
     851          21 :     if (!IAdviseReadCommon(arrayStartIdx, count, papszOptions, anIndicesCur,
     852             :                            nThreadsMax, anReqBlocksIndices, nReqBlocks))
     853             :     {
     854           2 :         return false;
     855             :     }
     856          19 :     if (nThreadsMax <= 1)
     857             :     {
     858           6 :         return true;
     859             :     }
     860             : 
     861             :     const int nThreads = static_cast<int>(
     862          13 :         std::min(static_cast<size_t>(nThreadsMax), nReqBlocks));
     863             : 
     864          13 :     CPLWorkerThreadPool *wtp = GDALGetGlobalThreadPool(nThreadsMax);
     865          13 :     if (wtp == nullptr)
     866           0 :         return false;
     867             : 
     868             :     struct JobStruct
     869             :     {
     870             :         JobStruct() = default;
     871             : 
     872             :         JobStruct(const JobStruct &) = delete;
     873             :         JobStruct &operator=(const JobStruct &) = delete;
     874             : 
     875             :         JobStruct(JobStruct &&) = default;
     876             :         JobStruct &operator=(JobStruct &&) = default;
     877             : 
     878             :         const ZarrV3Array *poArray = nullptr;
     879             :         bool *pbGlobalStatus = nullptr;
     880             :         int *pnRemainingThreads = nullptr;
     881             :         const std::vector<uint64_t> *panReqBlocksIndices = nullptr;
     882             :         size_t nFirstIdx = 0;
     883             :         size_t nLastIdxNotIncluded = 0;
     884             :     };
     885             : 
     886          13 :     std::vector<JobStruct> asJobStructs;
     887             : 
     888          13 :     bool bGlobalStatus = true;
     889          13 :     int nRemainingThreads = nThreads;
     890             :     // Check for very highly overflow in below loop
     891          13 :     assert(static_cast<size_t>(nThreads) <
     892             :            std::numeric_limits<size_t>::max() / nReqBlocks);
     893             : 
     894             :     // Setup jobs
     895          65 :     for (int i = 0; i < nThreads; i++)
     896             :     {
     897          52 :         JobStruct jobStruct;
     898          52 :         jobStruct.poArray = this;
     899          52 :         jobStruct.pbGlobalStatus = &bGlobalStatus;
     900          52 :         jobStruct.pnRemainingThreads = &nRemainingThreads;
     901          52 :         jobStruct.panReqBlocksIndices = &anReqBlocksIndices;
     902          52 :         jobStruct.nFirstIdx = static_cast<size_t>(i * nReqBlocks / nThreads);
     903          52 :         jobStruct.nLastIdxNotIncluded = std::min(
     904          52 :             static_cast<size_t>((i + 1) * nReqBlocks / nThreads), nReqBlocks);
     905          52 :         asJobStructs.emplace_back(std::move(jobStruct));
     906             :     }
     907             : 
     908          52 :     const auto JobFunc = [](void *pThreadData)
     909             :     {
     910          52 :         const JobStruct *jobStruct =
     911             :             static_cast<const JobStruct *>(pThreadData);
     912             : 
     913          52 :         const auto poArray = jobStruct->poArray;
     914          52 :         const size_t l_nDims = poArray->GetDimensionCount();
     915          52 :         ZarrByteVectorQuickResize abyRawBlockData;
     916          52 :         ZarrByteVectorQuickResize abyDecodedBlockData;
     917           0 :         std::unique_ptr<ZarrV3CodecSequence> poCodecs;
     918          52 :         if (poArray->m_poCodecs)
     919             :         {
     920          52 :             std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     921          52 :             poCodecs = poArray->m_poCodecs->Clone();
     922             :         }
     923             : 
     924       14904 :         for (size_t iReq = jobStruct->nFirstIdx;
     925       14904 :              iReq < jobStruct->nLastIdxNotIncluded; ++iReq)
     926             :         {
     927             :             // Check if we must early exit
     928             :             {
     929       14852 :                 std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     930       14852 :                 if (!(*jobStruct->pbGlobalStatus))
     931           0 :                     return;
     932             :             }
     933             : 
     934             :             const uint64_t *blockIndices =
     935       14852 :                 jobStruct->panReqBlocksIndices->data() + iReq * l_nDims;
     936             : 
     937       14852 :             if (!poArray->AllocateWorkingBuffers(abyRawBlockData,
     938             :                                                  abyDecodedBlockData))
     939             :             {
     940           0 :                 std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     941           0 :                 *jobStruct->pbGlobalStatus = false;
     942           0 :                 break;
     943             :             }
     944             : 
     945       14852 :             bool bIsEmpty = false;
     946       14852 :             bool success = poArray->LoadBlockData(
     947             :                 blockIndices,
     948             :                 true,  // use mutex
     949             :                 poCodecs.get(), abyRawBlockData, abyDecodedBlockData, bIsEmpty);
     950             : 
     951       14852 :             std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     952       14852 :             if (!success)
     953             :             {
     954           0 :                 *jobStruct->pbGlobalStatus = false;
     955           0 :                 break;
     956             :             }
     957             : 
     958       29704 :             CachedBlock cachedBlock;
     959       14852 :             if (!bIsEmpty)
     960             :             {
     961       14850 :                 if (!abyDecodedBlockData.empty())
     962           0 :                     std::swap(cachedBlock.abyDecoded, abyDecodedBlockData);
     963             :                 else
     964       14850 :                     std::swap(cachedBlock.abyDecoded, abyRawBlockData);
     965             :             }
     966             :             const std::vector<uint64_t> cacheKey{blockIndices,
     967       29704 :                                                  blockIndices + l_nDims};
     968       14852 :             poArray->m_oChunkCache[cacheKey] = std::move(cachedBlock);
     969             :         }
     970             : 
     971          52 :         std::lock_guard<std::mutex> oLock(poArray->m_oMutex);
     972          52 :         (*jobStruct->pnRemainingThreads)--;
     973             :     };
     974             : 
     975             :     // Start jobs
     976          65 :     for (int i = 0; i < nThreads; i++)
     977             :     {
     978          52 :         if (!wtp->SubmitJob(JobFunc, &asJobStructs[i]))
     979             :         {
     980           0 :             std::lock_guard<std::mutex> oLock(m_oMutex);
     981           0 :             bGlobalStatus = false;
     982           0 :             nRemainingThreads = i;
     983           0 :             break;
     984             :         }
     985             :     }
     986             : 
     987             :     // Wait for all jobs to be finished
     988             :     while (true)
     989             :     {
     990             :         {
     991          53 :             std::lock_guard<std::mutex> oLock(m_oMutex);
     992          53 :             if (nRemainingThreads == 0)
     993          13 :                 break;
     994             :         }
     995          40 :         wtp->WaitEvent();
     996          40 :     }
     997             : 
     998          13 :     return bGlobalStatus;
     999             : }
    1000             : 
    1001             : /************************************************************************/
    1002             : /*                    ZarrV3Array::FlushDirtyBlock()                    */
    1003             : /************************************************************************/
    1004             : 
    1005       18362 : bool ZarrV3Array::FlushDirtyBlock() const
    1006             : {
    1007       18362 :     if (!m_bDirtyBlock)
    1008        7378 :         return true;
    1009       10984 :     m_bDirtyBlock = false;
    1010             : 
    1011             :     // Sharded arrays need special handling: the block cache operates at
    1012             :     // inner chunk granularity but we must write complete shards.
    1013       10984 :     if (m_poCodecs && m_poCodecs->SupportsPartialDecoding())
    1014             :     {
    1015         108 :         return FlushDirtyBlockSharded();
    1016             :     }
    1017             : 
    1018       21752 :     std::string osFilename = BuildChunkFilename(m_anCachedBlockIndices.data());
    1019             : 
    1020             :     const size_t nSourceSize =
    1021       10876 :         m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
    1022       10876 :     const auto &abyBlock = m_abyDecodedBlockData.empty()
    1023             :                                ? m_abyRawBlockData
    1024       10876 :                                : m_abyDecodedBlockData;
    1025             : 
    1026       10876 :     if (IsEmptyBlock(abyBlock))
    1027             :     {
    1028           3 :         m_bCachedBlockEmpty = true;
    1029             : 
    1030             :         VSIStatBufL sStat;
    1031           3 :         if (VSIStatL(osFilename.c_str(), &sStat) == 0)
    1032             :         {
    1033           0 :             CPLDebugOnly(ZARR_DEBUG_KEY,
    1034             :                          "Deleting tile %s that has now empty content",
    1035             :                          osFilename.c_str());
    1036           0 :             return VSIUnlink(osFilename.c_str()) == 0;
    1037             :         }
    1038           3 :         return true;
    1039             :     }
    1040             : 
    1041       10873 :     if (!m_abyDecodedBlockData.empty())
    1042             :     {
    1043           0 :         const size_t nDTSize = m_oType.GetSize();
    1044           0 :         const size_t nValues = m_abyDecodedBlockData.size() / nDTSize;
    1045           0 :         GByte *pDst = &m_abyRawBlockData[0];
    1046           0 :         const GByte *pSrc = m_abyDecodedBlockData.data();
    1047           0 :         for (size_t i = 0; i < nValues;
    1048           0 :              i++, pDst += nSourceSize, pSrc += nDTSize)
    1049             :         {
    1050           0 :             EncodeElt(m_aoDtypeElts, pSrc, pDst);
    1051             :         }
    1052             :     }
    1053             : 
    1054       10873 :     const size_t nSizeBefore = m_abyRawBlockData.size();
    1055       10873 :     if (m_poCodecs)
    1056             :     {
    1057       10873 :         if (!m_poCodecs->Encode(m_abyRawBlockData))
    1058             :         {
    1059           0 :             m_abyRawBlockData.resize(nSizeBefore);
    1060           0 :             return false;
    1061             :         }
    1062             :     }
    1063             : 
    1064       10873 :     if (m_osDimSeparator == "/")
    1065             :     {
    1066       10873 :         std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
    1067             :         VSIStatBufL sStat;
    1068       10873 :         if (VSIStatL(osDir.c_str(), &sStat) != 0)
    1069             :         {
    1070         274 :             if (VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
    1071             :             {
    1072           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1073             :                          "Cannot create directory %s", osDir.c_str());
    1074           0 :                 m_abyRawBlockData.resize(nSizeBefore);
    1075           0 :                 return false;
    1076             :             }
    1077             :         }
    1078             :     }
    1079             : 
    1080       10873 :     VSILFILE *fp = VSIFOpenL(osFilename.c_str(), "wb");
    1081       10873 :     if (fp == nullptr)
    1082             :     {
    1083           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot create tile %s",
    1084             :                  osFilename.c_str());
    1085           0 :         m_abyRawBlockData.resize(nSizeBefore);
    1086           0 :         return false;
    1087             :     }
    1088             : 
    1089       10873 :     bool bRet = true;
    1090       10873 :     const size_t nRawDataSize = m_abyRawBlockData.size();
    1091       10873 :     if (VSIFWriteL(m_abyRawBlockData.data(), 1, nRawDataSize, fp) !=
    1092             :         nRawDataSize)
    1093             :     {
    1094           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    1095             :                  "Could not write tile %s correctly", osFilename.c_str());
    1096           0 :         bRet = false;
    1097             :     }
    1098       10873 :     VSIFCloseL(fp);
    1099             : 
    1100       10873 :     m_abyRawBlockData.resize(nSizeBefore);
    1101             : 
    1102       10873 :     return bRet;
    1103             : }
    1104             : 
    1105             : /************************************************************************/
    1106             : /*                ZarrV3Array::FlushDirtyBlockSharded()                 */
    1107             : /************************************************************************/
    1108             : 
    1109             : // Accumulates dirty inner chunks into a per-shard write cache.
    1110             : // Actual encoding and writing happens in FlushShardCache().
    1111             : // This avoids the O(N) decode-encode cost of re-encoding the full shard
    1112             : // for every inner chunk write (N = inner chunks per shard).
    1113             : // Single-writer only: concurrent writes to the same shard are not supported.
    1114             : 
    1115         108 : bool ZarrV3Array::FlushDirtyBlockSharded() const
    1116             : {
    1117         108 :     const size_t nDims = GetDimensionCount();
    1118             :     const size_t nSourceSize =
    1119         108 :         m_aoDtypeElts.back().nativeOffset + m_aoDtypeElts.back().nativeSize;
    1120             : 
    1121             :     // 1. Convert dirty inner block from GDAL format to native format
    1122         108 :     if (!m_abyDecodedBlockData.empty())
    1123             :     {
    1124           0 :         const size_t nDTSize = m_oType.GetSize();
    1125           0 :         const size_t nValues = m_abyDecodedBlockData.size() / nDTSize;
    1126           0 :         GByte *pDst = &m_abyRawBlockData[0];
    1127           0 :         const GByte *pSrc = m_abyDecodedBlockData.data();
    1128           0 :         for (size_t i = 0; i < nValues;
    1129           0 :              i++, pDst += nSourceSize, pSrc += nDTSize)
    1130             :         {
    1131           0 :             EncodeElt(m_aoDtypeElts, pSrc, pDst);
    1132             :         }
    1133             :     }
    1134             : 
    1135             :     // 2. Compute shard indices and inner block position within shard
    1136         216 :     std::vector<uint64_t> anShardIndices(nDims);
    1137         216 :     std::vector<size_t> anPosInShard(nDims);
    1138         348 :     for (size_t i = 0; i < nDims; ++i)
    1139             :     {
    1140         480 :         anShardIndices[i] = m_anCachedBlockIndices[i] * m_anInnerBlockSize[i] /
    1141         240 :                             m_anOuterBlockSize[i];
    1142         480 :         anPosInShard[i] = static_cast<size_t>(m_anCachedBlockIndices[i] %
    1143         240 :                                               m_anCountInnerBlockInOuter[i]);
    1144             :     }
    1145             : 
    1146         216 :     std::string osFilename = BuildChunkFilename(anShardIndices.data());
    1147             : 
    1148             :     // 3. Get or create shard cache entry
    1149         108 :     size_t nShardElements = 1;
    1150         348 :     for (size_t i = 0; i < nDims; ++i)
    1151         240 :         nShardElements *= static_cast<size_t>(m_anOuterBlockSize[i]);
    1152             : 
    1153         108 :     size_t nTotalInnerChunks = 1;
    1154         348 :     for (size_t i = 0; i < nDims; ++i)
    1155         240 :         nTotalInnerChunks *= static_cast<size_t>(m_anCountInnerBlockInOuter[i]);
    1156             : 
    1157         108 :     auto oIt = m_oShardWriteCache.find(osFilename);
    1158         108 :     if (oIt == m_oShardWriteCache.end())
    1159             :     {
    1160          37 :         ShardWriteEntry entry;
    1161             :         try
    1162             :         {
    1163          37 :             entry.abyShardBuffer.resize(nShardElements * nSourceSize);
    1164             :         }
    1165           0 :         catch (const std::exception &)
    1166             :         {
    1167           0 :             CPLError(CE_Failure, CPLE_OutOfMemory,
    1168             :                      "Cannot allocate memory for shard buffer");
    1169           0 :             return false;
    1170             :         }
    1171             :         try
    1172             :         {
    1173          37 :             entry.abDirtyInnerChunks.resize(nTotalInnerChunks, false);
    1174             :         }
    1175           0 :         catch (const std::exception &)
    1176             :         {
    1177           0 :             CPLError(CE_Failure, CPLE_OutOfMemory,
    1178             :                      "Cannot allocate memory for dirty chunk tracking");
    1179           0 :             return false;
    1180             :         }
    1181             : 
    1182             :         // Read existing shard or fill with nodata
    1183             :         VSIStatBufL sStat;
    1184          37 :         if (VSIStatL(osFilename.c_str(), &sStat) == 0)
    1185             :         {
    1186           1 :             VSILFILE *fpRead = VSIFOpenL(osFilename.c_str(), "rb");
    1187           1 :             if (fpRead == nullptr)
    1188             :             {
    1189           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1190             :                          "Cannot open shard file %s for reading",
    1191             :                          osFilename.c_str());
    1192           0 :                 return false;
    1193             :             }
    1194             : 
    1195           1 :             ZarrByteVectorQuickResize abyFileData;
    1196             :             try
    1197             :             {
    1198           1 :                 abyFileData.resize(static_cast<size_t>(sStat.st_size));
    1199             :             }
    1200           0 :             catch (const std::exception &)
    1201             :             {
    1202           0 :                 CPLError(
    1203             :                     CE_Failure, CPLE_OutOfMemory,
    1204             :                     "Cannot allocate " CPL_FRMT_GUIB " bytes for shard file %s",
    1205           0 :                     static_cast<GUIntBig>(sStat.st_size), osFilename.c_str());
    1206           0 :                 VSIFCloseL(fpRead);
    1207           0 :                 return false;
    1208             :             }
    1209           2 :             if (VSIFReadL(abyFileData.data(), 1, abyFileData.size(), fpRead) !=
    1210           1 :                 abyFileData.size())
    1211             :             {
    1212           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1213             :                          "Cannot read shard file %s", osFilename.c_str());
    1214           0 :                 VSIFCloseL(fpRead);
    1215           0 :                 return false;
    1216             :             }
    1217           1 :             VSIFCloseL(fpRead);
    1218             : 
    1219           1 :             entry.abyShardBuffer = std::move(abyFileData);
    1220           1 :             if (!m_poCodecs->Decode(entry.abyShardBuffer))
    1221             :             {
    1222           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1223             :                          "Cannot decode existing shard %s", osFilename.c_str());
    1224           0 :                 return false;
    1225             :             }
    1226             : 
    1227           1 :             if (entry.abyShardBuffer.size() != nShardElements * nSourceSize)
    1228             :             {
    1229           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1230             :                          "Decoded shard %s has unexpected size",
    1231             :                          osFilename.c_str());
    1232           0 :                 return false;
    1233             :             }
    1234             :         }
    1235             :         else
    1236             :         {
    1237          42 :             if (m_pabyNoData == nullptr ||
    1238           3 :                 (m_oType.GetClass() == GEDTC_NUMERIC &&
    1239           3 :                  GetNoDataValueAsDouble() == 0.0))
    1240             :             {
    1241          33 :                 memset(entry.abyShardBuffer.data(), 0,
    1242             :                        entry.abyShardBuffer.size());
    1243             :             }
    1244             :             else
    1245             :             {
    1246        4339 :                 for (size_t i = 0; i < nShardElements; ++i)
    1247             :                 {
    1248        4336 :                     memcpy(entry.abyShardBuffer.data() + i * nSourceSize,
    1249        4336 :                            m_pabyNoData, nSourceSize);
    1250             :                 }
    1251             :             }
    1252             :         }
    1253             : 
    1254          37 :         oIt = m_oShardWriteCache.emplace(osFilename, std::move(entry)).first;
    1255             :     }
    1256             : 
    1257             :     // cppcheck-suppress derefInvalidIteratorRedundantCheck
    1258         108 :     auto &entry = oIt->second;
    1259             : 
    1260             :     // 4. Compute inner chunk linear index and mark dirty
    1261         108 :     size_t nInnerChunkIdx = 0;
    1262         348 :     for (size_t i = 0; i < nDims; ++i)
    1263             :     {
    1264         480 :         nInnerChunkIdx = nInnerChunkIdx * static_cast<size_t>(
    1265         480 :                                               m_anCountInnerBlockInOuter[i]) +
    1266         240 :                          anPosInShard[i];
    1267             :     }
    1268         108 :     entry.abDirtyInnerChunks[nInnerChunkIdx] = true;
    1269             :     const bool bAllDirty =
    1270         108 :         std::all_of(entry.abDirtyInnerChunks.begin(),
    1271         341 :                     entry.abDirtyInnerChunks.end(), [](bool b) { return b; });
    1272             : 
    1273             :     // 5. Copy dirty inner block into shard buffer at correct position.
    1274             :     // Same strided N-D copy pattern as CopySubArrayIntoLargerOne() in
    1275             :     // zarr_v3_codec_sharding.cpp (operates on GUInt64 block sizes here).
    1276             :     {
    1277         216 :         std::vector<size_t> anShardStride(nDims);
    1278         108 :         size_t nStride = nSourceSize;
    1279         348 :         for (size_t iDim = nDims; iDim > 0;)
    1280             :         {
    1281         240 :             --iDim;
    1282         240 :             anShardStride[iDim] = nStride;
    1283         240 :             nStride *= static_cast<size_t>(m_anOuterBlockSize[iDim]);
    1284             :         }
    1285             : 
    1286         108 :         GByte *pShardDst = entry.abyShardBuffer.data();
    1287         348 :         for (size_t iDim = 0; iDim < nDims; ++iDim)
    1288             :         {
    1289         240 :             pShardDst += anPosInShard[iDim] *
    1290         480 :                          static_cast<size_t>(m_anInnerBlockSize[iDim]) *
    1291         240 :                          anShardStride[iDim];
    1292             :         }
    1293             : 
    1294         108 :         const GByte *pInnerSrc = m_abyRawBlockData.data();
    1295             :         const size_t nLastDimBytes =
    1296         108 :             static_cast<size_t>(m_anInnerBlockSize.back()) * nSourceSize;
    1297             : 
    1298         108 :         if (nDims == 1)
    1299             :         {
    1300           0 :             memcpy(pShardDst, pInnerSrc, nLastDimBytes);
    1301             :         }
    1302             :         else
    1303             :         {
    1304         216 :             std::vector<GByte *> dstPtrStack(nDims + 1);
    1305         216 :             std::vector<size_t> count(nDims + 1);
    1306         108 :             dstPtrStack[0] = pShardDst;
    1307         108 :             size_t dimIdx = 0;
    1308         755 :         lbl_next_depth:
    1309         755 :             if (dimIdx + 1 == nDims)
    1310             :             {
    1311         623 :                 memcpy(dstPtrStack[dimIdx], pInnerSrc, nLastDimBytes);
    1312         623 :                 pInnerSrc += nLastDimBytes;
    1313             :             }
    1314             :             else
    1315             :             {
    1316         132 :                 count[dimIdx] = static_cast<size_t>(m_anInnerBlockSize[dimIdx]);
    1317             :                 while (true)
    1318             :                 {
    1319         647 :                     dimIdx++;
    1320         647 :                     dstPtrStack[dimIdx] = dstPtrStack[dimIdx - 1];
    1321         647 :                     goto lbl_next_depth;
    1322         647 :                 lbl_return_to_caller:
    1323         647 :                     dimIdx--;
    1324         647 :                     if (--count[dimIdx] == 0)
    1325         132 :                         break;
    1326         515 :                     dstPtrStack[dimIdx] += anShardStride[dimIdx];
    1327             :                 }
    1328             :             }
    1329         755 :             if (dimIdx > 0)
    1330         647 :                 goto lbl_return_to_caller;
    1331             :         }
    1332             :     }
    1333             : 
    1334             :     // 6. Flush shard immediately if all inner chunks have been written,
    1335             :     // to bound memory usage during sequential writes.
    1336         108 :     if (bAllDirty)
    1337             :     {
    1338          18 :         const bool bOK = FlushSingleShard(osFilename, entry);
    1339          18 :         m_oShardWriteCache.erase(osFilename);
    1340          18 :         return bOK;
    1341             :     }
    1342             : 
    1343          90 :     return true;
    1344             : }
    1345             : 
    1346             : /************************************************************************/
    1347             : /*                   ZarrV3Array::FlushSingleShard()                    */
    1348             : /************************************************************************/
    1349             : 
    1350          37 : bool ZarrV3Array::FlushSingleShard(const std::string &osFilename,
    1351             :                                    ShardWriteEntry &entry) const
    1352             : {
    1353             :     // Encode mutates abyShardBuffer in-place. On failure the buffer
    1354             :     // is left in an undefined state, but the shard is not written.
    1355          37 :     if (!m_poCodecs->Encode(entry.abyShardBuffer))
    1356             :     {
    1357           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot encode shard for %s",
    1358             :                  osFilename.c_str());
    1359           0 :         return false;
    1360             :     }
    1361             : 
    1362             :     // All-nodata shard: skip writing (or delete stale file from prior write)
    1363          37 :     if (entry.abyShardBuffer.empty())
    1364             :     {
    1365             :         VSIStatBufL sStat;
    1366           0 :         if (VSIStatL(osFilename.c_str(), &sStat) == 0)
    1367           0 :             VSIUnlink(osFilename.c_str());
    1368           0 :         return true;
    1369             :     }
    1370             : 
    1371             :     // Create directory if needed
    1372          37 :     if (m_osDimSeparator == "/")
    1373             :     {
    1374          37 :         std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
    1375             :         VSIStatBufL sStatDir;
    1376          37 :         if (VSIStatL(osDir.c_str(), &sStatDir) != 0)
    1377             :         {
    1378          16 :             if (VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
    1379             :             {
    1380           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1381             :                          "Cannot create directory %s", osDir.c_str());
    1382           0 :                 return false;
    1383             :             }
    1384             :         }
    1385             :     }
    1386             : 
    1387          37 :     VSILFILE *fp = VSIFOpenL(osFilename.c_str(), "wb");
    1388          37 :     if (fp == nullptr)
    1389             :     {
    1390           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot create shard file %s",
    1391             :                  osFilename.c_str());
    1392           0 :         return false;
    1393             :     }
    1394             : 
    1395          37 :     const size_t nEncodedSize = entry.abyShardBuffer.size();
    1396          37 :     bool bRet = true;
    1397          37 :     if (VSIFWriteL(entry.abyShardBuffer.data(), 1, nEncodedSize, fp) !=
    1398             :         nEncodedSize)
    1399             :     {
    1400           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    1401             :                  "Could not write shard file %s correctly", osFilename.c_str());
    1402           0 :         bRet = false;
    1403             :     }
    1404          37 :     VSIFCloseL(fp);
    1405          37 :     if (bRet)
    1406          37 :         ZarrEraseShardIndexFromCache(osFilename);
    1407          37 :     return bRet;
    1408             : }
    1409             : 
    1410             : /************************************************************************/
    1411             : /*                    ZarrV3Array::FlushShardCache()                    */
    1412             : /************************************************************************/
    1413             : 
    1414             : // Encodes and writes all cached shards. Called from Flush().
    1415             : // Each shard is encoded exactly once regardless of how many inner chunks
    1416             : // were written.
    1417             : 
    1418        5886 : bool ZarrV3Array::FlushShardCache() const
    1419             : {
    1420        5886 :     if (m_oShardWriteCache.empty())
    1421        5879 :         return true;
    1422             : 
    1423           7 :     bool bRet = true;
    1424          26 :     for (auto &[osFilename, entry] : m_oShardWriteCache)
    1425             :     {
    1426          19 :         if (!FlushSingleShard(osFilename, entry))
    1427           0 :             bRet = false;
    1428             :     }
    1429             : 
    1430           7 :     m_oShardWriteCache.clear();
    1431           7 :     return bRet;
    1432             : }
    1433             : 
    1434             : /************************************************************************/
    1435             : /*                          ExtractSubArray()                           */
    1436             : /************************************************************************/
    1437             : 
    1438        4172 : static void ExtractSubArray(const GByte *const pabySrc,
    1439             :                             const std::vector<size_t> &anSrcStart,
    1440             :                             const std::vector<GPtrDiff_t> &anSrcStrideElts,
    1441             :                             const std::vector<size_t> &anCount,
    1442             :                             GByte *const pabyDst,
    1443             :                             const std::vector<GPtrDiff_t> &anDstStrideElts,
    1444             :                             const size_t nDTSize)
    1445             : {
    1446        4172 :     const auto nDims = anSrcStart.size();
    1447        4172 :     CPLAssert(nDims > 0);
    1448        4172 :     CPLAssert(nDims == anSrcStrideElts.size());
    1449        4172 :     CPLAssert(nDims == anCount.size());
    1450        4172 :     CPLAssert(nDims == anDstStrideElts.size());
    1451             : 
    1452             : #if defined(__GNUC__)
    1453             : #pragma GCC diagnostic push
    1454             : #pragma GCC diagnostic ignored "-Wnull-dereference"
    1455             : #endif
    1456        8344 :     std::vector<const GByte *> srcPtrStack(nDims);
    1457        8344 :     std::vector<GByte *> dstPtrStack(nDims);
    1458        8344 :     std::vector<GPtrDiff_t> anSrcStrideBytes(nDims);
    1459        8344 :     std::vector<GPtrDiff_t> anDstStrideBytes(nDims);
    1460        8344 :     std::vector<size_t> count(nDims);
    1461             : 
    1462        4172 :     srcPtrStack[0] = pabySrc;
    1463       16688 :     for (size_t i = 0; i < nDims; ++i)
    1464             :     {
    1465       12516 :         anSrcStrideBytes[i] = anSrcStrideElts[i] * nDTSize;
    1466       12516 :         anDstStrideBytes[i] = anDstStrideElts[i] * nDTSize;
    1467       12516 :         srcPtrStack[0] += anSrcStart[i] * anSrcStrideBytes[i];
    1468             :     }
    1469        4172 :     dstPtrStack[0] = pabyDst;
    1470             : #if defined(__GNUC__)
    1471             : #pragma GCC diagnostic pop
    1472             : #endif
    1473             : 
    1474        4172 :     const size_t nLastDimSize = anCount.back() * nDTSize;
    1475        4172 :     size_t dimIdx = 0;
    1476      357989 : lbl_next_depth:
    1477      357989 :     if (dimIdx + 1 == nDims)
    1478             :     {
    1479      341847 :         memcpy(dstPtrStack[dimIdx], srcPtrStack[dimIdx], nLastDimSize);
    1480             :     }
    1481             :     else
    1482             :     {
    1483       16142 :         count[dimIdx] = anCount[dimIdx];
    1484             :         while (true)
    1485             :         {
    1486      353817 :             dimIdx++;
    1487      353817 :             srcPtrStack[dimIdx] = srcPtrStack[dimIdx - 1];
    1488      353817 :             dstPtrStack[dimIdx] = dstPtrStack[dimIdx - 1];
    1489      353817 :             goto lbl_next_depth;
    1490      353817 :         lbl_return_to_caller:
    1491      353817 :             dimIdx--;
    1492      353817 :             if (--count[dimIdx] == 0)
    1493       16142 :                 break;
    1494      337675 :             srcPtrStack[dimIdx] += anSrcStrideBytes[dimIdx];
    1495      337675 :             dstPtrStack[dimIdx] += anDstStrideBytes[dimIdx];
    1496             :         }
    1497             :     }
    1498      357989 :     if (dimIdx > 0)
    1499      353817 :         goto lbl_return_to_caller;
    1500        4172 : }
    1501             : 
    1502             : /************************************************************************/
    1503             : /*                 ZarrV3Array::WriteChunksThreadSafe()                 */
    1504             : /************************************************************************/
    1505             : 
    1506          36 : bool ZarrV3Array::WriteChunksThreadSafe(
    1507             :     const GUInt64 *arrayStartIdx, const size_t *count,
    1508             :     [[maybe_unused]] const GInt64 *arrayStep, const GPtrDiff_t *bufferStride,
    1509             :     [[maybe_unused]] const GDALExtendedDataType &bufferDataType,
    1510             :     const void *pSrcBuffer, const int iThread, const int nThreads,
    1511             :     std::string &osErrorMsg) const
    1512             : {
    1513          36 :     CPLAssert(m_oType == bufferDataType);
    1514             : 
    1515          36 :     const auto nDims = GetDimensionCount();
    1516          72 :     std::vector<size_t> anChunkCount(nDims);
    1517          72 :     std::vector<size_t> anChunkCoord(nDims);
    1518          36 :     size_t nChunks = 1;
    1519         144 :     for (size_t i = 0; i < nDims; ++i)
    1520             :     {
    1521         108 :         CPLAssert(count[i] == 1 || arrayStep[i] == 1);
    1522         108 :         anChunkCount[i] = static_cast<size_t>(cpl::div_round_up(
    1523         108 :             static_cast<uint64_t>(count[i]), m_anOuterBlockSize[i]));
    1524         108 :         nChunks *= anChunkCount[i];
    1525             :     }
    1526             : 
    1527          36 :     const size_t iFirstChunk = static_cast<size_t>(
    1528          36 :         (static_cast<uint64_t>(iThread) * nChunks) / nThreads);
    1529          36 :     const size_t iLastChunkExcluded = static_cast<size_t>(
    1530          36 :         (static_cast<uint64_t>(iThread + 1) * nChunks) / nThreads);
    1531             : 
    1532          72 :     std::vector<size_t> anSrcStart(nDims);
    1533             :     const std::vector<GPtrDiff_t> anSrcStrideElts(bufferStride,
    1534          72 :                                                   bufferStride + nDims);
    1535          72 :     std::vector<GPtrDiff_t> anDstStrideElts(nDims);
    1536             : 
    1537          36 :     size_t nDstStride = 1;
    1538         144 :     for (size_t i = nDims, iChunkCur = iFirstChunk; i > 0;)
    1539             :     {
    1540         108 :         --i;
    1541         108 :         anChunkCoord[i] = iChunkCur % anChunkCount[i];
    1542         108 :         iChunkCur /= anChunkCount[i];
    1543             : 
    1544         108 :         anDstStrideElts[i] = nDstStride;
    1545         108 :         nDstStride *= static_cast<size_t>(m_anOuterBlockSize[i]);
    1546             :     }
    1547             : 
    1548           3 :     const auto StoreError = [this, &osErrorMsg](const std::string &s)
    1549             :     {
    1550           1 :         std::lock_guard oLock(m_oMutex);
    1551           1 :         if (!osErrorMsg.empty())
    1552           0 :             osErrorMsg += '\n';
    1553           1 :         osErrorMsg = s;
    1554           2 :         return false;
    1555          36 :     };
    1556             : 
    1557          36 :     const size_t nDTSize = m_oType.GetSize();
    1558             :     const size_t nDstSize =
    1559          36 :         static_cast<size_t>(MultiplyElements(m_anOuterBlockSize)) * nDTSize;
    1560          72 :     ZarrByteVectorQuickResize abyDst;
    1561             :     try
    1562             :     {
    1563          36 :         abyDst.resize(nDstSize);
    1564             :     }
    1565           0 :     catch (const std::exception &)
    1566             :     {
    1567           0 :         return StoreError("Out of memory allocating temporary buffer");
    1568             :     }
    1569             : 
    1570          36 :     std::unique_ptr<ZarrV3CodecSequence> poCodecs;
    1571          36 :     if (m_poCodecs)
    1572             :     {
    1573             :         // Codec cloning is not thread safe
    1574          36 :         std::lock_guard oLock(m_oMutex);
    1575          36 :         poCodecs = m_poCodecs->Clone();
    1576             :     }
    1577             : 
    1578          72 :     std::vector<uint64_t> anChunkIndex(nDims);
    1579          72 :     std::vector<size_t> anCount(nDims);
    1580        4207 :     for (size_t iChunk = iFirstChunk; iChunk < iLastChunkExcluded; ++iChunk)
    1581             :     {
    1582        4172 :         if (iChunk > iFirstChunk)
    1583             :         {
    1584        4136 :             size_t iDimToIncrement = nDims - 1;
    1585        8346 :             while (++anChunkCoord[iDimToIncrement] ==
    1586        4173 :                    anChunkCount[iDimToIncrement])
    1587             :             {
    1588          37 :                 anChunkCoord[iDimToIncrement] = 0;
    1589          37 :                 CPLAssert(iDimToIncrement >= 1);
    1590          37 :                 --iDimToIncrement;
    1591             :             }
    1592             :         }
    1593             : 
    1594        4172 :         bool bPartialChunk = false;
    1595       16688 :         for (size_t i = 0; i < nDims; ++i)
    1596             :         {
    1597       25032 :             anChunkIndex[i] =
    1598       12516 :                 anChunkCoord[i] + arrayStartIdx[i] / m_anOuterBlockSize[i];
    1599       25032 :             anSrcStart[i] =
    1600       12516 :                 anChunkCoord[i] * static_cast<size_t>(m_anOuterBlockSize[i]);
    1601       12516 :             anCount[i] = static_cast<size_t>(std::min(
    1602       12516 :                 m_aoDims[i]->GetSize() - arrayStartIdx[i] - anSrcStart[i],
    1603       25032 :                 m_anOuterBlockSize[i]));
    1604       12516 :             bPartialChunk = bPartialChunk || anCount[i] < m_anOuterBlockSize[i];
    1605             :         }
    1606             : 
    1607             :         // Resize to target size, as a previous iteration may have shorten it
    1608             :         // during compression.
    1609        4172 :         abyDst.resize(nDstSize);
    1610        4172 :         if (bPartialChunk)
    1611        3832 :             memset(abyDst.data(), 0, nDstSize);
    1612             : 
    1613        4172 :         ExtractSubArray(static_cast<const GByte *>(pSrcBuffer), anSrcStart,
    1614             :                         anSrcStrideElts, anCount, abyDst.data(),
    1615             :                         anDstStrideElts, nDTSize);
    1616             : 
    1617        4172 :         const std::string osFilename = BuildChunkFilename(anChunkIndex.data());
    1618        4172 :         if (IsEmptyBlock(abyDst))
    1619             :         {
    1620             :             VSIStatBufL sStat;
    1621           0 :             if (VSIStatL(osFilename.c_str(), &sStat) == 0)
    1622             :             {
    1623           0 :                 CPLDebugOnly(ZARR_DEBUG_KEY,
    1624             :                              "Deleting chunk %s that has now empty content",
    1625             :                              osFilename.c_str());
    1626           0 :                 if (VSIUnlink(osFilename.c_str()) != 0)
    1627             :                 {
    1628           0 :                     return StoreError("Chunk " + osFilename +
    1629           0 :                                       " deletion failed");
    1630             :                 }
    1631             :             }
    1632           0 :             continue;
    1633             :         }
    1634             : 
    1635        4172 :         if (poCodecs)
    1636             :         {
    1637        4172 :             CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    1638        4172 :             if (!poCodecs->Encode(abyDst))
    1639             :             {
    1640           0 :                 return StoreError(CPLGetLastErrorMsg());
    1641             :             }
    1642             :         }
    1643             : 
    1644        4172 :         if (m_osDimSeparator == "/")
    1645             :         {
    1646        4172 :             const std::string osDir = CPLGetDirnameSafe(osFilename.c_str());
    1647             :             VSIStatBufL sStat;
    1648        4221 :             if (VSIStatL(osDir.c_str(), &sStat) != 0 &&
    1649          49 :                 VSIMkdirRecursive(osDir.c_str(), 0755) != 0)
    1650             :             {
    1651           0 :                 return StoreError("Cannot create directory " + osDir);
    1652             :             }
    1653             :         }
    1654             : 
    1655        4172 :         auto fp = VSIFilesystemHandler::OpenStatic(osFilename.c_str(), "wb");
    1656        4172 :         if (fp == nullptr)
    1657             :         {
    1658           1 :             return StoreError("Cannot create file " + osFilename);
    1659             :         }
    1660             : 
    1661        8342 :         if (fp->Write(abyDst.data(), abyDst.size()) != abyDst.size() ||
    1662        4171 :             fp->Close() != 0)
    1663             :         {
    1664           0 :             return StoreError("Write error while writing " + osFilename);
    1665             :         }
    1666             :     }
    1667             : 
    1668          35 :     return true;
    1669             : }
    1670             : 
    1671             : /************************************************************************/
    1672             : /*                        ZarrV3Array::IWrite()                         */
    1673             : /************************************************************************/
    1674             : 
    1675         146 : bool ZarrV3Array::IWrite(const GUInt64 *arrayStartIdx, const size_t *count,
    1676             :                          const GInt64 *arrayStep,
    1677             :                          const GPtrDiff_t *bufferStride,
    1678             :                          const GDALExtendedDataType &bufferDataType,
    1679             :                          const void *pSrcBuffer)
    1680             : {
    1681             : 
    1682             :     // Multithreading writing if window is aligned on chunk boundaries.
    1683         146 :     if (m_oType == bufferDataType && m_oType.GetClass() == GEDTC_NUMERIC)
    1684             :     {
    1685         135 :         const auto nDims = GetDimensionCount();
    1686         135 :         bool bCanUseMultiThreading = true;
    1687         135 :         size_t nChunks = 1;
    1688         396 :         for (size_t i = 0; i < nDims; ++i)
    1689             :         {
    1690         269 :             if ((arrayStartIdx[i] % m_anOuterBlockSize[i]) != 0 ||
    1691         535 :                 (count[i] != 1 && arrayStep[i] != 1) ||
    1692         301 :                 !((count[i] % m_anOuterBlockSize[i]) == 0 ||
    1693          35 :                   arrayStartIdx[i] + count[i] == m_aoDims[i]->GetSize()))
    1694             :             {
    1695           8 :                 bCanUseMultiThreading = false;
    1696           8 :                 break;
    1697             :             }
    1698         261 :             nChunks *= static_cast<size_t>(cpl::div_round_up(
    1699         261 :                 static_cast<uint64_t>(count[i]), m_anOuterBlockSize[i]));
    1700             :         }
    1701         135 :         if (bCanUseMultiThreading && nChunks >= 2)
    1702             :         {
    1703             :             const int nMaxThreads = static_cast<int>(
    1704          30 :                 std::min<size_t>(nChunks, GDAL_DEFAULT_MAX_THREAD_COUNT));
    1705             :             const int nThreads =
    1706          30 :                 GDALGetNumThreads(nMaxThreads, /* bDefaultAllCPUs=*/false);
    1707             :             CPLWorkerThreadPool *wtp =
    1708          30 :                 nThreads >= 2 ? GDALGetGlobalThreadPool(nThreads) : nullptr;
    1709             : 
    1710          30 :             if (wtp)
    1711             :             {
    1712           9 :                 m_oChunkCache.clear();
    1713             : 
    1714           9 :                 if (!FlushDirtyBlock())
    1715           0 :                     return false;
    1716             : 
    1717           9 :                 CPLDebug("Zarr", "Using %d threads for writing", nThreads);
    1718          18 :                 auto poJobQueue = wtp->CreateJobQueue();
    1719           9 :                 std::atomic<bool> bSuccess = true;
    1720          18 :                 std::string osErrorMsg;
    1721          45 :                 for (int iThread = 0; iThread < nThreads; ++iThread)
    1722             :                 {
    1723          36 :                     auto job = [this, iThread, nThreads, arrayStartIdx, count,
    1724             :                                 arrayStep, bufferStride, pSrcBuffer,
    1725          73 :                                 &bufferDataType, &bSuccess, &osErrorMsg]()
    1726             :                     {
    1727          72 :                         if (bSuccess &&
    1728          36 :                             !WriteChunksThreadSafe(
    1729             :                                 arrayStartIdx, count, arrayStep, bufferStride,
    1730             :                                 bufferDataType, pSrcBuffer, iThread, nThreads,
    1731          36 :                                 osErrorMsg))
    1732             :                         {
    1733           1 :                             bSuccess = false;
    1734             :                         }
    1735          72 :                     };
    1736          36 :                     if (!poJobQueue->SubmitJob(job))
    1737             :                     {
    1738           0 :                         CPLError(
    1739             :                             CE_Failure, CPLE_AppDefined,
    1740             :                             "ZarrV3Array::IWrite(): job submission failed");
    1741           0 :                         return false;
    1742             :                     }
    1743             :                 }
    1744             : 
    1745           9 :                 poJobQueue->WaitCompletion();
    1746             : 
    1747           9 :                 if (!bSuccess)
    1748             :                 {
    1749           1 :                     CPLError(CE_Failure, CPLE_AppDefined,
    1750             :                              "ZarrV3Array::IWrite(): %s", osErrorMsg.c_str());
    1751             :                 }
    1752             : 
    1753           9 :                 return bSuccess;
    1754             :             }
    1755             :         }
    1756             :     }
    1757             : 
    1758         137 :     return ZarrArray::IWrite(arrayStartIdx, count, arrayStep, bufferStride,
    1759         137 :                              bufferDataType, pSrcBuffer);
    1760             : }
    1761             : 
    1762             : /************************************************************************/
    1763             : /*                         BuildChunkFilename()                         */
    1764             : /************************************************************************/
    1765             : 
    1766       44444 : std::string ZarrV3Array::BuildChunkFilename(const uint64_t *blockIndices) const
    1767             : {
    1768       44444 :     if (m_aoDims.empty())
    1769             :     {
    1770             :         return CPLFormFilenameSafe(
    1771           0 :             CPLGetDirnameSafe(m_osFilename.c_str()).c_str(),
    1772           0 :             m_bV2ChunkKeyEncoding ? "0" : "c", nullptr);
    1773             :     }
    1774             :     else
    1775             :     {
    1776       88888 :         std::string osFilename(CPLGetDirnameSafe(m_osFilename.c_str()));
    1777       44444 :         osFilename += '/';
    1778       44444 :         if (!m_bV2ChunkKeyEncoding)
    1779             :         {
    1780       44435 :             osFilename += 'c';
    1781             :         }
    1782      141773 :         for (size_t i = 0; i < m_aoDims.size(); ++i)
    1783             :         {
    1784       97329 :             if (i > 0 || !m_bV2ChunkKeyEncoding)
    1785       97320 :                 osFilename += m_osDimSeparator;
    1786       97329 :             osFilename += std::to_string(blockIndices[i]);
    1787             :         }
    1788       44444 :         return osFilename;
    1789             :     }
    1790             : }
    1791             : 
    1792             : /************************************************************************/
    1793             : /*                          GetDataDirectory()                          */
    1794             : /************************************************************************/
    1795             : 
    1796           5 : std::string ZarrV3Array::GetDataDirectory() const
    1797             : {
    1798           5 :     return std::string(CPLGetDirnameSafe(m_osFilename.c_str()));
    1799             : }
    1800             : 
    1801             : /************************************************************************/
    1802             : /*                    GetChunkIndicesFromFilename()                     */
    1803             : /************************************************************************/
    1804             : 
    1805             : CPLStringList
    1806          19 : ZarrV3Array::GetChunkIndicesFromFilename(const char *pszFilename) const
    1807             : {
    1808          19 :     if (!m_bV2ChunkKeyEncoding)
    1809             :     {
    1810          19 :         if (pszFilename[0] != 'c')
    1811           6 :             return CPLStringList();
    1812          13 :         if (m_osDimSeparator == "/")
    1813             :         {
    1814          13 :             if (pszFilename[1] != '/' && pszFilename[1] != '\\')
    1815           0 :                 return CPLStringList();
    1816             :         }
    1817           0 :         else if (pszFilename[1] != m_osDimSeparator[0])
    1818             :         {
    1819           0 :             return CPLStringList();
    1820             :         }
    1821             :     }
    1822             :     return CPLStringList(
    1823          13 :         CSLTokenizeString2(pszFilename + (m_bV2ChunkKeyEncoding ? 0 : 2),
    1824          26 :                            m_osDimSeparator.c_str(), 0));
    1825             : }
    1826             : 
    1827             : /************************************************************************/
    1828             : /*                            ParseDtypeV3()                            */
    1829             : /************************************************************************/
    1830             : 
    1831        1167 : static GDALExtendedDataType ParseDtypeV3(const CPLJSONObject &obj,
    1832             :                                          std::vector<DtypeElt> &elts)
    1833             : {
    1834             :     do
    1835             :     {
    1836        1167 :         if (obj.GetType() == CPLJSONObject::Type::String)
    1837             :         {
    1838        2318 :             const auto str = obj.ToString();
    1839        1159 :             DtypeElt elt;
    1840        1159 :             GDALDataType eDT = GDT_Unknown;
    1841             : 
    1842        1159 :             if (str == "bool")  // boolean
    1843             :             {
    1844           0 :                 elt.nativeType = DtypeElt::NativeType::BOOLEAN;
    1845           0 :                 eDT = GDT_UInt8;
    1846             :             }
    1847        1159 :             else if (str == "int8")
    1848             :             {
    1849           6 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1850           6 :                 eDT = GDT_Int8;
    1851             :             }
    1852        1153 :             else if (str == "uint8")
    1853             :             {
    1854         116 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1855         116 :                 eDT = GDT_UInt8;
    1856             :             }
    1857        1037 :             else if (str == "int16")
    1858             :             {
    1859          11 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1860          11 :                 eDT = GDT_Int16;
    1861             :             }
    1862        1026 :             else if (str == "uint16")
    1863             :             {
    1864           9 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1865           9 :                 eDT = GDT_UInt16;
    1866             :             }
    1867        1017 :             else if (str == "int32")
    1868             :             {
    1869           7 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1870           7 :                 eDT = GDT_Int32;
    1871             :             }
    1872        1010 :             else if (str == "uint32")
    1873             :             {
    1874           7 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1875           7 :                 eDT = GDT_UInt32;
    1876             :             }
    1877        1003 :             else if (str == "int64")
    1878             :             {
    1879         146 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1880         146 :                 eDT = GDT_Int64;
    1881             :             }
    1882         857 :             else if (str == "uint64")
    1883             :             {
    1884           6 :                 elt.nativeType = DtypeElt::NativeType::UNSIGNED_INT;
    1885           6 :                 eDT = GDT_UInt64;
    1886             :             }
    1887         851 :             else if (str == "float16")
    1888             :             {
    1889           3 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1890           3 :                 eDT = GDT_Float16;
    1891             :             }
    1892         848 :             else if (str == "float32")
    1893             :             {
    1894         782 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1895         782 :                 eDT = GDT_Float32;
    1896             :             }
    1897          66 :             else if (str == "float64")
    1898             :             {
    1899          27 :                 elt.nativeType = DtypeElt::NativeType::IEEEFP;
    1900          27 :                 eDT = GDT_Float64;
    1901             :             }
    1902          39 :             else if (str == "complex64")
    1903             :             {
    1904          15 :                 elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
    1905          15 :                 eDT = GDT_CFloat32;
    1906             :             }
    1907          24 :             else if (str == "complex128")
    1908             :             {
    1909          15 :                 elt.nativeType = DtypeElt::NativeType::COMPLEX_IEEEFP;
    1910          15 :                 eDT = GDT_CFloat64;
    1911             :             }
    1912           9 :             else if (str == "string")
    1913             :             {
    1914             :                 // Variable-length UTF-8 strings (zarr-extensions).
    1915             :                 // nativeSize determines the fixed slot size used by the
    1916             :                 // vlen-utf8 codec; strings exceeding (nativeSize-1) are
    1917             :                 // truncated.  Increase via ZARR_VLEN_STRING_MAX_LENGTH.
    1918             :                 const int nMaxLen =
    1919           8 :                     std::max(2, atoi(CPLGetConfigOption(
    1920           8 :                                     "ZARR_VLEN_STRING_MAX_LENGTH", "256")));
    1921           8 :                 elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
    1922           8 :                 elt.nativeSize = static_cast<size_t>(nMaxLen);
    1923          16 :                 elt.gdalType = GDALExtendedDataType::CreateString(
    1924           8 :                     static_cast<size_t>(nMaxLen));
    1925           8 :                 elt.gdalSize = elt.gdalType.GetSize();
    1926           8 :                 elts.emplace_back(elt);
    1927             :                 return GDALExtendedDataType::CreateString(
    1928           8 :                     static_cast<size_t>(nMaxLen));
    1929             :             }
    1930             :             else
    1931           1 :                 break;
    1932             : 
    1933        1150 :             elt.gdalType = GDALExtendedDataType::Create(eDT);
    1934        1150 :             elt.gdalSize = elt.gdalType.GetSize();
    1935        1150 :             if (!elt.gdalTypeIsApproxOfNative)
    1936        1150 :                 elt.nativeSize = elt.gdalSize;
    1937        1150 :             if (elt.nativeSize > 1)
    1938             :             {
    1939        1028 :                 elt.needByteSwapping = (CPL_IS_LSB == 0);
    1940             :             }
    1941        1150 :             elts.emplace_back(elt);
    1942        1150 :             return GDALExtendedDataType::Create(eDT);
    1943             :         }
    1944           8 :         else if (obj.GetType() == CPLJSONObject::Type::Object)
    1945             :         {
    1946          16 :             const auto osName = obj["name"].ToString();
    1947          16 :             const auto oConfig = obj["configuration"];
    1948           8 :             DtypeElt elt;
    1949             : 
    1950           8 :             if (osName == "null_terminated_bytes" && oConfig.IsValid())
    1951             :             {
    1952           2 :                 const int nBytes = oConfig["length_bytes"].ToInteger();
    1953           2 :                 if (nBytes <= 0 || nBytes > 10 * 1024 * 1024)
    1954             :                     break;
    1955           2 :                 elt.nativeType = DtypeElt::NativeType::STRING_ASCII;
    1956           2 :                 elt.nativeSize = static_cast<size_t>(nBytes);
    1957           4 :                 elt.gdalType = GDALExtendedDataType::CreateString(
    1958           2 :                     static_cast<size_t>(nBytes));
    1959           2 :                 elt.gdalSize = elt.gdalType.GetSize();
    1960           2 :                 elts.emplace_back(elt);
    1961             :                 return GDALExtendedDataType::CreateString(
    1962           2 :                     static_cast<size_t>(nBytes));
    1963             :             }
    1964           6 :             else if (osName == "fixed_length_utf32" && oConfig.IsValid())
    1965             :             {
    1966           1 :                 const int nBytes = oConfig["length_bytes"].ToInteger();
    1967           1 :                 if (nBytes <= 0 || nBytes % 4 != 0 || nBytes > 10 * 1024 * 1024)
    1968             :                     break;
    1969           1 :                 elt.nativeType = DtypeElt::NativeType::STRING_UNICODE;
    1970           1 :                 elt.nativeSize = static_cast<size_t>(nBytes);
    1971             :                 // Endianness handled by the bytes codec in v3
    1972           1 :                 elt.gdalType = GDALExtendedDataType::CreateString();
    1973           1 :                 elt.gdalSize = elt.gdalType.GetSize();
    1974           1 :                 elts.emplace_back(elt);
    1975           1 :                 return GDALExtendedDataType::CreateString();
    1976             :             }
    1977           8 :             else if (osName == "numpy.datetime64" ||
    1978           3 :                      osName == "numpy.timedelta64")
    1979             :             {
    1980           4 :                 elt.nativeType = DtypeElt::NativeType::SIGNED_INT;
    1981           4 :                 elt.gdalType = GDALExtendedDataType::Create(GDT_Int64);
    1982           4 :                 elt.gdalSize = elt.gdalType.GetSize();
    1983           4 :                 elt.nativeSize = elt.gdalSize;
    1984           4 :                 elt.needByteSwapping = (CPL_IS_LSB == 0);
    1985           4 :                 elts.emplace_back(elt);
    1986           4 :                 return GDALExtendedDataType::Create(GDT_Int64);
    1987             :             }
    1988             :         }
    1989             :     } while (false);
    1990           2 :     CPLError(CE_Failure, CPLE_AppDefined,
    1991             :              "Invalid or unsupported format for data_type: %s",
    1992           4 :              obj.ToString().c_str());
    1993           2 :     return GDALExtendedDataType::Create(GDT_Unknown);
    1994             : }
    1995             : 
    1996             : /************************************************************************/
    1997             : /*                     ParseNoDataStringAsDouble()                      */
    1998             : /************************************************************************/
    1999             : 
    2000         297 : static double ParseNoDataStringAsDouble(const std::string &osVal, bool &bOK)
    2001             : {
    2002         297 :     double dfNoDataValue = std::numeric_limits<double>::quiet_NaN();
    2003         297 :     if (osVal == "NaN")
    2004             :     {
    2005             :         // initialized above
    2006             :     }
    2007          15 :     else if (osVal == "Infinity" || osVal == "+Infinity")
    2008             :     {
    2009           5 :         dfNoDataValue = std::numeric_limits<double>::infinity();
    2010             :     }
    2011          10 :     else if (osVal == "-Infinity")
    2012             :     {
    2013           5 :         dfNoDataValue = -std::numeric_limits<double>::infinity();
    2014             :     }
    2015             :     else
    2016             :     {
    2017           5 :         bOK = false;
    2018             :     }
    2019         297 :     return dfNoDataValue;
    2020             : }
    2021             : 
    2022             : /************************************************************************/
    2023             : /*                        ParseNoDataComponent()                        */
    2024             : /************************************************************************/
    2025             : 
    2026             : template <typename T, typename Tint>
    2027          40 : static T ParseNoDataComponent(const CPLJSONObject &oObj, bool &bOK)
    2028             : {
    2029          40 :     if (oObj.GetType() == CPLJSONObject::Type::Integer ||
    2030          62 :         oObj.GetType() == CPLJSONObject::Type::Long ||
    2031          22 :         oObj.GetType() == CPLJSONObject::Type::Double)
    2032             :     {
    2033          22 :         return static_cast<T>(oObj.ToDouble());
    2034             :     }
    2035          18 :     else if (oObj.GetType() == CPLJSONObject::Type::String)
    2036             :     {
    2037          54 :         const auto osVal = oObj.ToString();
    2038          18 :         if (STARTS_WITH(osVal.c_str(), "0x"))
    2039             :         {
    2040           2 :             if (osVal.size() > 2 + 2 * sizeof(T))
    2041             :             {
    2042           0 :                 bOK = false;
    2043           0 :                 return 0;
    2044             :             }
    2045           2 :             Tint nVal = static_cast<Tint>(
    2046           2 :                 std::strtoull(osVal.c_str() + 2, nullptr, 16));
    2047             :             T fVal;
    2048             :             static_assert(sizeof(nVal) == sizeof(fVal),
    2049             :                           "sizeof(nVal) == sizeof(dfVal)");
    2050           2 :             memcpy(&fVal, &nVal, sizeof(nVal));
    2051           2 :             return fVal;
    2052             :         }
    2053             :         else
    2054             :         {
    2055          16 :             return static_cast<T>(ParseNoDataStringAsDouble(osVal, bOK));
    2056             :         }
    2057             :     }
    2058             :     else
    2059             :     {
    2060           0 :         bOK = false;
    2061           0 :         return 0;
    2062             :     }
    2063             : }
    2064             : 
    2065             : /************************************************************************/
    2066             : /*                       ZarrV3Group::LoadArray()                       */
    2067             : /************************************************************************/
    2068             : 
    2069             : std::shared_ptr<ZarrArray>
    2070        1180 : ZarrV3Group::LoadArray(const std::string &osArrayName,
    2071             :                        const std::string &osZarrayFilename,
    2072             :                        const CPLJSONObject &oRoot) const
    2073             : {
    2074             :     // Add osZarrayFilename to m_poSharedResource during the scope
    2075             :     // of this function call.
    2076        1180 :     ZarrSharedResource::SetFilenameAdder filenameAdder(m_poSharedResource,
    2077        2360 :                                                        osZarrayFilename);
    2078        1180 :     if (!filenameAdder.ok())
    2079           0 :         return nullptr;
    2080             : 
    2081             :     // Warn about unknown members (the spec suggests to error out, but let be
    2082             :     // a bit more lenient)
    2083       12940 :     for (const auto &oNode : oRoot.GetChildren())
    2084             :     {
    2085       23520 :         const auto osName = oNode.GetName();
    2086       31740 :         if (osName != "zarr_format" && osName != "node_type" &&
    2087       24663 :             osName != "shape" && osName != "chunk_grid" &&
    2088       17589 :             osName != "data_type" && osName != "chunk_key_encoding" &&
    2089        8188 :             osName != "fill_value" &&
    2090             :             // Below are optional
    2091        8578 :             osName != "dimension_names" && osName != "codecs" &&
    2092       24318 :             osName != "storage_transformers" && osName != "attributes")
    2093             :         {
    2094           4 :             CPLError(CE_Warning, CPLE_AppDefined,
    2095             :                      "%s array definition contains a unknown member (%s). "
    2096             :                      "Interpretation of the array might be wrong.",
    2097             :                      osZarrayFilename.c_str(), osName.c_str());
    2098             :         }
    2099             :     }
    2100             : 
    2101        3540 :     const auto oStorageTransformers = oRoot["storage_transformers"].ToArray();
    2102        1180 :     if (oStorageTransformers.Size() > 0)
    2103             :     {
    2104           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2105             :                  "storage_transformers are not supported.");
    2106           1 :         return nullptr;
    2107             :     }
    2108             : 
    2109        3537 :     const auto oShape = oRoot["shape"].ToArray();
    2110        1179 :     if (!oShape.IsValid())
    2111             :     {
    2112           2 :         CPLError(CE_Failure, CPLE_AppDefined, "shape missing or not an array");
    2113           2 :         return nullptr;
    2114             :     }
    2115             : 
    2116             :     // Parse chunk_grid
    2117        3531 :     const auto oChunkGrid = oRoot["chunk_grid"];
    2118        1177 :     if (oChunkGrid.GetType() != CPLJSONObject::Type::Object)
    2119             :     {
    2120           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2121             :                  "chunk_grid missing or not an object");
    2122           1 :         return nullptr;
    2123             :     }
    2124             : 
    2125        3528 :     const auto oChunkGridName = oChunkGrid["name"];
    2126        1176 :     if (oChunkGridName.ToString() != "regular")
    2127             :     {
    2128           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2129             :                  "Only chunk_grid.name = regular supported");
    2130           1 :         return nullptr;
    2131             :     }
    2132             : 
    2133        3525 :     const auto oChunks = oChunkGrid["configuration"]["chunk_shape"].ToArray();
    2134        1175 :     if (!oChunks.IsValid())
    2135             :     {
    2136           1 :         CPLError(
    2137             :             CE_Failure, CPLE_AppDefined,
    2138             :             "chunk_grid.configuration.chunk_shape missing or not an array");
    2139           1 :         return nullptr;
    2140             :     }
    2141             : 
    2142        1174 :     if (oShape.Size() != oChunks.Size())
    2143             :     {
    2144           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2145             :                  "shape and chunks arrays are of different size");
    2146           1 :         return nullptr;
    2147             :     }
    2148             : 
    2149             :     // Parse chunk_key_encoding
    2150        3519 :     const auto oChunkKeyEncoding = oRoot["chunk_key_encoding"];
    2151        1173 :     if (oChunkKeyEncoding.GetType() != CPLJSONObject::Type::Object)
    2152             :     {
    2153           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2154             :                  "chunk_key_encoding missing or not an object");
    2155           1 :         return nullptr;
    2156             :     }
    2157             : 
    2158        2344 :     std::string osDimSeparator;
    2159        1172 :     bool bV2ChunkKeyEncoding = false;
    2160        3516 :     const auto oChunkKeyEncodingName = oChunkKeyEncoding["name"];
    2161        1172 :     if (oChunkKeyEncodingName.ToString() == "default")
    2162             :     {
    2163        1159 :         osDimSeparator = "/";
    2164             :     }
    2165          13 :     else if (oChunkKeyEncodingName.ToString() == "v2")
    2166             :     {
    2167          12 :         osDimSeparator = ".";
    2168          12 :         bV2ChunkKeyEncoding = true;
    2169             :     }
    2170             :     else
    2171             :     {
    2172           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2173             :                  "Unsupported chunk_key_encoding.name");
    2174           1 :         return nullptr;
    2175             :     }
    2176             : 
    2177             :     {
    2178        2342 :         auto oConfiguration = oChunkKeyEncoding["configuration"];
    2179        1171 :         if (oConfiguration.GetType() == CPLJSONObject::Type::Object)
    2180             :         {
    2181        2222 :             auto oSeparator = oConfiguration["separator"];
    2182        1111 :             if (oSeparator.IsValid())
    2183             :             {
    2184        1111 :                 osDimSeparator = oSeparator.ToString();
    2185        1111 :                 if (osDimSeparator != "/" && osDimSeparator != ".")
    2186             :                 {
    2187           1 :                     CPLError(CE_Failure, CPLE_AppDefined,
    2188             :                              "Separator can only be '/' or '.'");
    2189           1 :                     return nullptr;
    2190             :                 }
    2191             :             }
    2192             :         }
    2193             :     }
    2194             : 
    2195        3510 :     CPLJSONObject oAttributes = oRoot["attributes"];
    2196             : 
    2197             :     // Deep-clone of oAttributes
    2198        1170 :     if (oAttributes.IsValid())
    2199             :     {
    2200        1092 :         oAttributes = oAttributes.Clone();
    2201             :     }
    2202             : 
    2203        2340 :     std::vector<std::shared_ptr<GDALDimension>> aoDims;
    2204        3316 :     for (int i = 0; i < oShape.Size(); ++i)
    2205             :     {
    2206        2146 :         const auto nSize = static_cast<GUInt64>(oShape[i].ToLong());
    2207        2146 :         if (nSize == 0)
    2208             :         {
    2209           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Invalid content for shape");
    2210           0 :             return nullptr;
    2211             :         }
    2212        2146 :         aoDims.emplace_back(std::make_shared<ZarrDimension>(
    2213        2146 :             m_poSharedResource,
    2214        4292 :             std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
    2215        4292 :             std::string(), CPLSPrintf("dim%d", i), std::string(), std::string(),
    2216        2146 :             nSize));
    2217             :     }
    2218             : 
    2219             :     // Deal with dimension_names
    2220        3510 :     const auto dimensionNames = oRoot["dimension_names"];
    2221             : 
    2222         722 :     const auto FindDimension = [this, &aoDims, &osArrayName, &oAttributes](
    2223             :                                    const std::string &osDimName,
    2224        7778 :                                    std::shared_ptr<GDALDimension> &poDim, int i)
    2225             :     {
    2226         722 :         auto oIter = m_oMapDimensions.find(osDimName);
    2227         722 :         if (oIter != m_oMapDimensions.end())
    2228             :         {
    2229         308 :             if (m_bDimSizeInUpdate ||
    2230         154 :                 oIter->second->GetSize() == poDim->GetSize())
    2231             :             {
    2232         154 :                 poDim = oIter->second;
    2233         154 :                 return true;
    2234             :             }
    2235             :             else
    2236             :             {
    2237           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
    2238             :                          "Size of _ARRAY_DIMENSIONS[%d] different "
    2239             :                          "from the one of shape",
    2240             :                          i);
    2241           0 :                 return false;
    2242             :             }
    2243             :         }
    2244             : 
    2245             :         // Try to load the indexing variable.
    2246             :         // Not in m_oMapMDArrays,
    2247             :         // then stat() the indexing variable.
    2248        1054 :         else if (osArrayName != osDimName &&
    2249        1054 :                  m_oMapMDArrays.find(osDimName) == m_oMapMDArrays.end())
    2250             :         {
    2251         972 :             std::string osDirName = m_osDirectoryName;
    2252             :             while (true)
    2253             :             {
    2254             :                 const std::string osArrayFilenameDim = CPLFormFilenameSafe(
    2255        2184 :                     CPLFormFilenameSafe(osDirName.c_str(), osDimName.c_str(),
    2256             :                                         nullptr)
    2257             :                         .c_str(),
    2258        2184 :                     "zarr.json", nullptr);
    2259             :                 VSIStatBufL sStat;
    2260        2184 :                 if (VSIStatL(osArrayFilenameDim.c_str(), &sStat) == 0)
    2261             :                 {
    2262           4 :                     CPLJSONDocument oDoc;
    2263           2 :                     if (oDoc.Load(osArrayFilenameDim))
    2264             :                     {
    2265           2 :                         LoadArray(osDimName, osArrayFilenameDim,
    2266           4 :                                   oDoc.GetRoot());
    2267             :                     }
    2268             :                 }
    2269             :                 else
    2270             :                 {
    2271             :                     // Recurse to upper level for datasets such as
    2272             :                     // /vsis3/hrrrzarr/sfc/20210809/20210809_00z_anl.zarr/0.1_sigma_level/HAIL_max_fcst/0.1_sigma_level/HAIL_max_fcst
    2273             :                     std::string osDirNameNew =
    2274        2182 :                         CPLGetPathSafe(osDirName.c_str());
    2275        2182 :                     if (!osDirNameNew.empty() && osDirNameNew != osDirName)
    2276             :                     {
    2277        1698 :                         osDirName = std::move(osDirNameNew);
    2278        1698 :                         continue;
    2279             :                     }
    2280             :                 }
    2281         486 :                 break;
    2282        1698 :             }
    2283             :         }
    2284             : 
    2285         568 :         oIter = m_oMapDimensions.find(osDimName);
    2286             :         // cppcheck-suppress knownConditionTrueFalse
    2287         570 :         if (oIter != m_oMapDimensions.end() &&
    2288           2 :             oIter->second->GetSize() == poDim->GetSize())
    2289             :         {
    2290           2 :             poDim = oIter->second;
    2291           2 :             return true;
    2292             :         }
    2293             : 
    2294        1132 :         std::string osType;
    2295        1132 :         std::string osDirection;
    2296         566 :         if (aoDims.size() == 1 && osArrayName == osDimName)
    2297             :         {
    2298          82 :             ZarrArray::GetDimensionTypeDirection(oAttributes, osType,
    2299             :                                                  osDirection);
    2300             :         }
    2301             : 
    2302             :         auto poDimLocal = std::make_shared<ZarrDimension>(
    2303         566 :             m_poSharedResource,
    2304        1132 :             std::dynamic_pointer_cast<ZarrGroupBase>(m_pSelf.lock()),
    2305        1132 :             GetFullName(), osDimName, osType, osDirection, poDim->GetSize());
    2306         566 :         poDimLocal->SetXArrayDimension();
    2307         566 :         m_oMapDimensions[osDimName] = poDimLocal;
    2308         566 :         poDim = poDimLocal;
    2309         566 :         return true;
    2310        1170 :     };
    2311             : 
    2312        1170 :     if (dimensionNames.GetType() == CPLJSONObject::Type::Array)
    2313             :     {
    2314         407 :         const auto arrayDims = dimensionNames.ToArray();
    2315         407 :         if (arrayDims.Size() == oShape.Size())
    2316             :         {
    2317        1128 :             for (int i = 0; i < oShape.Size(); ++i)
    2318             :             {
    2319         722 :                 if (arrayDims[i].GetType() == CPLJSONObject::Type::String)
    2320             :                 {
    2321        2166 :                     const auto osDimName = arrayDims[i].ToString();
    2322         722 :                     FindDimension(osDimName, aoDims[i], i);
    2323             :                 }
    2324             :             }
    2325             :         }
    2326             :         else
    2327             :         {
    2328           1 :             CPLError(
    2329             :                 CE_Failure, CPLE_AppDefined,
    2330             :                 "Size of dimension_names[] different from the one of shape");
    2331           1 :             return nullptr;
    2332             :         }
    2333             :     }
    2334         763 :     else if (dimensionNames.IsValid())
    2335             :     {
    2336           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    2337             :                  "dimension_names should be an array");
    2338           1 :         return nullptr;
    2339             :     }
    2340             : 
    2341        3504 :     auto oDtype = oRoot["data_type"];
    2342        1168 :     if (!oDtype.IsValid())
    2343             :     {
    2344           1 :         CPLError(CE_Failure, CPLE_NotSupported, "data_type missing");
    2345           1 :         return nullptr;
    2346             :     }
    2347        2334 :     const auto oOrigDtype = oDtype;
    2348        1167 :     if (oDtype["fallback"].IsValid())
    2349           1 :         oDtype = oDtype["fallback"];
    2350        2334 :     std::vector<DtypeElt> aoDtypeElts;
    2351        2334 :     const auto oType = ParseDtypeV3(oDtype, aoDtypeElts);
    2352        2323 :     if (oType.GetClass() == GEDTC_NUMERIC &&
    2353        1156 :         oType.GetNumericDataType() == GDT_Unknown)
    2354           2 :         return nullptr;
    2355             : 
    2356        2330 :     std::vector<GUInt64> anOuterBlockSize;
    2357        1165 :     if (!ZarrArray::ParseChunkSize(oChunks, oType, anOuterBlockSize))
    2358           1 :         return nullptr;
    2359             : 
    2360        2328 :     std::vector<GByte> abyNoData;
    2361             : 
    2362             :     struct NoDataFreer
    2363             :     {
    2364             :         std::vector<GByte> &m_abyNodata;
    2365             :         const GDALExtendedDataType &m_oType;
    2366             : 
    2367        1164 :         NoDataFreer(std::vector<GByte> &abyNoDataIn,
    2368             :                     const GDALExtendedDataType &oTypeIn)
    2369        1164 :             : m_abyNodata(abyNoDataIn), m_oType(oTypeIn)
    2370             :         {
    2371        1164 :         }
    2372             : 
    2373        1164 :         ~NoDataFreer()
    2374        1164 :         {
    2375        1164 :             if (!m_abyNodata.empty())
    2376        1019 :                 m_oType.FreeDynamicMemory(&m_abyNodata[0]);
    2377        1164 :         }
    2378             :     };
    2379             : 
    2380        2328 :     NoDataFreer noDataFreer(abyNoData, oType);
    2381             : 
    2382        3492 :     auto oFillValue = oRoot["fill_value"];
    2383        1164 :     auto eFillValueType = oFillValue.GetType();
    2384             : 
    2385        1164 :     if (!oFillValue.IsValid())
    2386             :     {
    2387           0 :         CPLError(CE_Warning, CPLE_AppDefined, "Missing fill_value is invalid");
    2388             :     }
    2389        1164 :     else if (eFillValueType == CPLJSONObject::Type::Null)
    2390             :     {
    2391         129 :         CPLError(CE_Warning, CPLE_AppDefined, "fill_value = null is invalid");
    2392             :     }
    2393        1035 :     else if (GDALDataTypeIsComplex(oType.GetNumericDataType()) &&
    2394             :              eFillValueType != CPLJSONObject::Type::Array)
    2395             :     {
    2396           4 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2397           4 :         return nullptr;
    2398             :     }
    2399        1031 :     else if (eFillValueType == CPLJSONObject::Type::String)
    2400             :     {
    2401         594 :         const auto osFillValue = oFillValue.ToString();
    2402         297 :         if (oType.GetClass() == GEDTC_STRING)
    2403             :         {
    2404           6 :             abyNoData.resize(oType.GetSize());
    2405           6 :             char *pDstStr = CPLStrdup(osFillValue.c_str());
    2406           6 :             char **pDstPtr = reinterpret_cast<char **>(&abyNoData[0]);
    2407           6 :             memcpy(pDstPtr, &pDstStr, sizeof(pDstStr));
    2408             :         }
    2409         291 :         else if (STARTS_WITH(osFillValue.c_str(), "0x"))
    2410             :         {
    2411           3 :             if (osFillValue.size() > 2 + 2 * oType.GetSize())
    2412             :             {
    2413           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2414           1 :                 return nullptr;
    2415             :             }
    2416             :             uint64_t nVal = static_cast<uint64_t>(
    2417           3 :                 std::strtoull(osFillValue.c_str() + 2, nullptr, 16));
    2418           3 :             if (oType.GetSize() == 4)
    2419             :             {
    2420           1 :                 abyNoData.resize(oType.GetSize());
    2421           1 :                 uint32_t nTmp = static_cast<uint32_t>(nVal);
    2422           1 :                 memcpy(&abyNoData[0], &nTmp, sizeof(nTmp));
    2423             :             }
    2424           2 :             else if (oType.GetSize() == 8)
    2425             :             {
    2426           1 :                 abyNoData.resize(oType.GetSize());
    2427           1 :                 memcpy(&abyNoData[0], &nVal, sizeof(nVal));
    2428             :             }
    2429             :             else
    2430             :             {
    2431           1 :                 CPLError(CE_Failure, CPLE_AppDefined,
    2432             :                          "Hexadecimal representation of fill_value no "
    2433             :                          "supported for this data type");
    2434           1 :                 return nullptr;
    2435             :             }
    2436             :         }
    2437         288 :         else if (STARTS_WITH(osFillValue.c_str(), "0b"))
    2438             :         {
    2439           3 :             if (osFillValue.size() > 2 + 8 * oType.GetSize())
    2440             :             {
    2441           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2442           1 :                 return nullptr;
    2443             :             }
    2444             :             uint64_t nVal = static_cast<uint64_t>(
    2445           3 :                 std::strtoull(osFillValue.c_str() + 2, nullptr, 2));
    2446           3 :             if (oType.GetSize() == 4)
    2447             :             {
    2448           1 :                 abyNoData.resize(oType.GetSize());
    2449           1 :                 uint32_t nTmp = static_cast<uint32_t>(nVal);
    2450           1 :                 memcpy(&abyNoData[0], &nTmp, sizeof(nTmp));
    2451             :             }
    2452           2 :             else if (oType.GetSize() == 8)
    2453             :             {
    2454           1 :                 abyNoData.resize(oType.GetSize());
    2455           1 :                 memcpy(&abyNoData[0], &nVal, sizeof(nVal));
    2456             :             }
    2457             :             else
    2458             :             {
    2459           1 :                 CPLError(CE_Failure, CPLE_AppDefined,
    2460             :                          "Binary representation of fill_value no supported for "
    2461             :                          "this data type");
    2462           1 :                 return nullptr;
    2463             :             }
    2464             :         }
    2465             :         else
    2466             :         {
    2467             :             // Handle "NaT" fill_value for numpy.datetime64/timedelta64
    2468             :             // NaT is equivalent to INT64_MIN per the zarr extension spec
    2469         285 :             if (osFillValue == "NaT" && oType.GetNumericDataType() == GDT_Int64)
    2470             :             {
    2471           4 :                 const int64_t nNaT = std::numeric_limits<int64_t>::min();
    2472           4 :                 abyNoData.resize(oType.GetSize());
    2473           4 :                 memcpy(&abyNoData[0], &nNaT, sizeof(nNaT));
    2474             :             }
    2475             :             else
    2476             :             {
    2477         281 :                 bool bOK = true;
    2478             :                 double dfNoDataValue =
    2479         281 :                     ParseNoDataStringAsDouble(osFillValue, bOK);
    2480         281 :                 if (!bOK)
    2481             :                 {
    2482           1 :                     CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2483           2 :                     return nullptr;
    2484             :                 }
    2485         280 :                 else if (oType.GetNumericDataType() == GDT_Float16)
    2486             :                 {
    2487             :                     const GFloat16 hfNoDataValue =
    2488           1 :                         static_cast<GFloat16>(dfNoDataValue);
    2489           1 :                     abyNoData.resize(sizeof(hfNoDataValue));
    2490           1 :                     memcpy(&abyNoData[0], &hfNoDataValue,
    2491             :                            sizeof(hfNoDataValue));
    2492             :                 }
    2493         279 :                 else if (oType.GetNumericDataType() == GDT_Float32)
    2494             :                 {
    2495         254 :                     const float fNoDataValue =
    2496         254 :                         static_cast<float>(dfNoDataValue);
    2497         254 :                     abyNoData.resize(sizeof(fNoDataValue));
    2498         254 :                     memcpy(&abyNoData[0], &fNoDataValue, sizeof(fNoDataValue));
    2499             :                 }
    2500          25 :                 else if (oType.GetNumericDataType() == GDT_Float64)
    2501             :                 {
    2502          24 :                     abyNoData.resize(sizeof(dfNoDataValue));
    2503          24 :                     memcpy(&abyNoData[0], &dfNoDataValue,
    2504             :                            sizeof(dfNoDataValue));
    2505             :                 }
    2506             :                 else
    2507             :                 {
    2508           1 :                     CPLError(CE_Failure, CPLE_AppDefined,
    2509             :                              "Invalid fill_value for this data type");
    2510           1 :                     return nullptr;
    2511             :                 }
    2512             :             }
    2513             :         }
    2514             :     }
    2515         734 :     else if (eFillValueType == CPLJSONObject::Type::Boolean ||
    2516         535 :              eFillValueType == CPLJSONObject::Type::Integer ||
    2517         535 :              eFillValueType == CPLJSONObject::Type::Long ||
    2518             :              eFillValueType == CPLJSONObject::Type::Double)
    2519             :     {
    2520         710 :         const double dfNoDataValue = oFillValue.ToDouble();
    2521         710 :         if (oType.GetNumericDataType() == GDT_Int64)
    2522             :         {
    2523             :             const int64_t nNoDataValue =
    2524         140 :                 static_cast<int64_t>(oFillValue.ToLong());
    2525         140 :             abyNoData.resize(oType.GetSize());
    2526         140 :             GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
    2527             :                           oType.GetNumericDataType(), 0, 1);
    2528             :         }
    2529         570 :         else if (oType.GetNumericDataType() == GDT_UInt64 &&
    2530             :                  /* we can't really deal with nodata value between */
    2531             :                  /* int64::max and uint64::max due to json-c limitations */
    2532           0 :                  dfNoDataValue >= 0)
    2533             :         {
    2534             :             const int64_t nNoDataValue =
    2535           0 :                 static_cast<int64_t>(oFillValue.ToLong());
    2536           0 :             abyNoData.resize(oType.GetSize());
    2537           0 :             GDALCopyWords(&nNoDataValue, GDT_Int64, 0, &abyNoData[0],
    2538             :                           oType.GetNumericDataType(), 0, 1);
    2539             :         }
    2540             :         else
    2541             :         {
    2542         570 :             abyNoData.resize(oType.GetSize());
    2543         570 :             GDALCopyWords(&dfNoDataValue, GDT_Float64, 0, &abyNoData[0],
    2544             :                           oType.GetNumericDataType(), 0, 1);
    2545         710 :         }
    2546             :     }
    2547          24 :     else if (eFillValueType == CPLJSONObject::Type::Array)
    2548             :     {
    2549          24 :         const auto oFillValueArray = oFillValue.ToArray();
    2550          44 :         if (oFillValueArray.Size() == 2 &&
    2551          20 :             GDALDataTypeIsComplex(oType.GetNumericDataType()))
    2552             :         {
    2553          20 :             if (oType.GetNumericDataType() == GDT_CFloat64)
    2554             :             {
    2555          10 :                 bool bOK = true;
    2556             :                 const double adfNoDataValue[2] = {
    2557          10 :                     ParseNoDataComponent<double, uint64_t>(oFillValueArray[0],
    2558             :                                                            bOK),
    2559          10 :                     ParseNoDataComponent<double, uint64_t>(oFillValueArray[1],
    2560             :                                                            bOK),
    2561          20 :                 };
    2562          10 :                 if (!bOK)
    2563             :                 {
    2564           2 :                     CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2565           2 :                     return nullptr;
    2566             :                 }
    2567           8 :                 abyNoData.resize(oType.GetSize());
    2568           8 :                 CPLAssert(sizeof(adfNoDataValue) == oType.GetSize());
    2569           8 :                 memcpy(abyNoData.data(), adfNoDataValue,
    2570             :                        sizeof(adfNoDataValue));
    2571             :             }
    2572             :             else
    2573             :             {
    2574          10 :                 CPLAssert(oType.GetNumericDataType() == GDT_CFloat32);
    2575          10 :                 bool bOK = true;
    2576             :                 const float afNoDataValue[2] = {
    2577          10 :                     ParseNoDataComponent<float, uint32_t>(oFillValueArray[0],
    2578             :                                                           bOK),
    2579          10 :                     ParseNoDataComponent<float, uint32_t>(oFillValueArray[1],
    2580             :                                                           bOK),
    2581          20 :                 };
    2582          10 :                 if (!bOK)
    2583             :                 {
    2584           2 :                     CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2585           2 :                     return nullptr;
    2586             :                 }
    2587           8 :                 abyNoData.resize(oType.GetSize());
    2588           8 :                 CPLAssert(sizeof(afNoDataValue) == oType.GetSize());
    2589           8 :                 memcpy(abyNoData.data(), afNoDataValue, sizeof(afNoDataValue));
    2590             :             }
    2591             :         }
    2592             :         else
    2593             :         {
    2594           4 :             CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2595           4 :             return nullptr;
    2596             :         }
    2597             :     }
    2598             :     else
    2599             :     {
    2600           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Invalid fill_value");
    2601           0 :         return nullptr;
    2602             :     }
    2603             : 
    2604        3444 :     const auto oCodecs = oRoot["codecs"].ToArray();
    2605        1148 :     std::unique_ptr<ZarrV3CodecSequence> poCodecs;
    2606        2296 :     std::vector<GUInt64> anInnerBlockSize = anOuterBlockSize;
    2607        1148 :     if (oCodecs.Size() > 0)
    2608             :     {
    2609        2236 :         poCodecs = ZarrV3Array::SetupCodecs(oCodecs, anOuterBlockSize,
    2610             :                                             anInnerBlockSize,
    2611        2236 :                                             aoDtypeElts.back(), abyNoData);
    2612        1118 :         if (!poCodecs)
    2613             :         {
    2614          18 :             return nullptr;
    2615             :         }
    2616             :     }
    2617             : 
    2618        1130 :     auto poArray = ZarrV3Array::Create(m_poSharedResource, Self(), osArrayName,
    2619             :                                        aoDims, oType, aoDtypeElts,
    2620        2260 :                                        anOuterBlockSize, anInnerBlockSize);
    2621        1130 :     if (!poArray)
    2622           1 :         return nullptr;
    2623        1129 :     poArray->SetUpdatable(m_bUpdatable);  // must be set before SetAttributes()
    2624        1129 :     poArray->SetFilename(osZarrayFilename);
    2625        1129 :     poArray->SetIsV2ChunkKeyEncoding(bV2ChunkKeyEncoding);
    2626        1129 :     poArray->SetDimSeparator(osDimSeparator);
    2627        1129 :     if (!abyNoData.empty())
    2628             :     {
    2629        1000 :         poArray->RegisterNoDataValue(abyNoData.data());
    2630             :     }
    2631        1129 :     poArray->SetAttributes(Self(), oAttributes);
    2632        1129 :     poArray->SetDtype(oDtype);
    2633             :     // Expose extension data type configuration as structural info
    2634        1129 :     if (oOrigDtype.GetType() == CPLJSONObject::Type::Object)
    2635             :     {
    2636          24 :         const auto osName = oOrigDtype.GetString("name");
    2637           8 :         if (!osName.empty())
    2638             :         {
    2639           8 :             poArray->SetStructuralInfo("data_type.name", osName.c_str());
    2640             :         }
    2641          24 :         const auto oConfig = oOrigDtype["configuration"];
    2642          16 :         if (oConfig.IsValid() &&
    2643           8 :             oConfig.GetType() == CPLJSONObject::Type::Object)
    2644             :         {
    2645          24 :             const auto osUnit = oConfig.GetString("unit");
    2646           8 :             if (!osUnit.empty())
    2647             :             {
    2648           5 :                 poArray->SetStructuralInfo("data_type.unit", osUnit.c_str());
    2649             :             }
    2650           8 :             const auto nScaleFactor = oConfig.GetInteger("scale_factor", -1);
    2651           8 :             if (nScaleFactor > 0)
    2652             :             {
    2653           4 :                 poArray->SetStructuralInfo("data_type.scale_factor",
    2654             :                                            CPLSPrintf("%d", nScaleFactor));
    2655             :             }
    2656             :         }
    2657             :     }
    2658        2229 :     if (oCodecs.Size() > 0 &&
    2659        2229 :         oCodecs[oCodecs.Size() - 1].GetString("name") != "bytes")
    2660             :     {
    2661        1396 :         poArray->SetStructuralInfo(
    2662        1396 :             "COMPRESSOR", oCodecs[oCodecs.Size() - 1].ToString().c_str());
    2663             :     }
    2664        1129 :     if (poCodecs)
    2665        1100 :         poArray->SetCodecs(oCodecs, std::move(poCodecs));
    2666        1129 :     RegisterArray(poArray);
    2667             : 
    2668             :     // If this is an indexing variable, attach it to the dimension.
    2669        1129 :     if (aoDims.size() == 1 && aoDims[0]->GetName() == poArray->GetName())
    2670             :     {
    2671          82 :         auto oIter = m_oMapDimensions.find(poArray->GetName());
    2672          82 :         if (oIter != m_oMapDimensions.end())
    2673             :         {
    2674          82 :             oIter->second->SetIndexingVariable(poArray);
    2675             :         }
    2676             :     }
    2677             : 
    2678        1129 :     if (CPLTestBool(m_poSharedResource->GetOpenOptions().FetchNameValueDef(
    2679             :             "CACHE_TILE_PRESENCE", "NO")))
    2680             :     {
    2681           5 :         poArray->BlockCachePresence();
    2682             :     }
    2683             : 
    2684        1129 :     return poArray;
    2685             : }
    2686             : 
    2687             : /************************************************************************/
    2688             : /*                  ZarrV3Array::GetRawBlockInfoInfo()                  */
    2689             : /************************************************************************/
    2690             : 
    2691           6 : CPLStringList ZarrV3Array::GetRawBlockInfoInfo() const
    2692             : {
    2693           6 :     CPLStringList aosInfo(m_aosStructuralInfo);
    2694           6 :     if (m_oType.GetSize() > 1)
    2695             :     {
    2696             :         // By default, assume that the ENDIANNESS is the native one.
    2697             :         // Otherwise there will be a ZarrV3CodecBytes instance.
    2698             :         if constexpr (CPL_IS_LSB)
    2699           5 :             aosInfo.SetNameValue("ENDIANNESS", "LITTLE");
    2700             :         else
    2701             :             aosInfo.SetNameValue("ENDIANNESS", "BIG");
    2702             :     }
    2703             : 
    2704           6 :     if (m_poCodecs)
    2705             :     {
    2706           6 :         bool bHasOtherCodec = false;
    2707          11 :         for (const auto &poCodec : m_poCodecs->GetCodecs())
    2708             :         {
    2709           6 :             if (poCodec->GetName() == ZarrV3CodecBytes::NAME &&
    2710           1 :                 m_oType.GetSize() > 1)
    2711             :             {
    2712             :                 auto poBytesCodec =
    2713           1 :                     dynamic_cast<const ZarrV3CodecBytes *>(poCodec.get());
    2714           1 :                 if (poBytesCodec)
    2715             :                 {
    2716           1 :                     if (poBytesCodec->IsLittle())
    2717           0 :                         aosInfo.SetNameValue("ENDIANNESS", "LITTLE");
    2718             :                     else
    2719           1 :                         aosInfo.SetNameValue("ENDIANNESS", "BIG");
    2720             :                 }
    2721             :             }
    2722           5 :             else if (poCodec->GetName() == ZarrV3CodecTranspose::NAME &&
    2723           1 :                      m_aoDims.size() > 1)
    2724             :             {
    2725             :                 auto poTransposeCodec =
    2726           1 :                     dynamic_cast<const ZarrV3CodecTranspose *>(poCodec.get());
    2727           1 :                 if (poTransposeCodec && !poTransposeCodec->IsNoOp())
    2728             :                 {
    2729           1 :                     const auto &anOrder = poTransposeCodec->GetOrder();
    2730           1 :                     const int nDims = static_cast<int>(anOrder.size());
    2731           2 :                     std::string osOrder("[");
    2732           3 :                     for (int i = 0; i < nDims; ++i)
    2733             :                     {
    2734           2 :                         if (i > 0)
    2735           1 :                             osOrder += ',';
    2736           2 :                         osOrder += std::to_string(anOrder[i]);
    2737             :                     }
    2738           1 :                     osOrder += ']';
    2739           1 :                     aosInfo.SetNameValue("TRANSPOSE_ORDER", osOrder.c_str());
    2740             :                 }
    2741             :             }
    2742           5 :             else if (poCodec->GetName() != ZarrV3CodecGZip::NAME &&
    2743           5 :                      poCodec->GetName() != ZarrV3CodecBlosc::NAME &&
    2744           2 :                      poCodec->GetName() != ZarrV3CodecZstd::NAME)
    2745             :             {
    2746           2 :                 bHasOtherCodec = true;
    2747             :             }
    2748             :         }
    2749           6 :         if (bHasOtherCodec)
    2750             :         {
    2751           2 :             aosInfo.SetNameValue("CODECS", m_oJSONCodecs.ToString().c_str());
    2752             :         }
    2753             : 
    2754           6 :         if (m_poCodecs->SupportsPartialDecoding())
    2755             :         {
    2756           2 :             aosInfo.SetNameValue("CHUNK_TYPE", "INNER");
    2757             :         }
    2758             :     }
    2759             : 
    2760           6 :     return aosInfo;
    2761             : }
    2762             : 
    2763             : /************************************************************************/
    2764             : /*                      ZarrV3Array::SetupCodecs()                      */
    2765             : /************************************************************************/
    2766             : 
    2767        1312 : /* static */ std::unique_ptr<ZarrV3CodecSequence> ZarrV3Array::SetupCodecs(
    2768             :     const CPLJSONArray &oCodecs, const std::vector<GUInt64> &anOuterBlockSize,
    2769             :     std::vector<GUInt64> &anInnerBlockSize, DtypeElt &zarrDataType,
    2770             :     const std::vector<GByte> &abyNoData)
    2771             : 
    2772             : {
    2773             :     // Byte swapping will be done by the codec chain
    2774        1312 :     zarrDataType.needByteSwapping = false;
    2775             : 
    2776        2624 :     ZarrArrayMetadata oInputArrayMetadata;
    2777        1312 :     if (!abyNoData.empty() && zarrDataType.gdalTypeIsApproxOfNative)
    2778             :     {
    2779             :         // This cannot happen today with the data types we support, but
    2780             :         // might in the future. In which case we'll have to translate the
    2781             :         // nodata value from its GDAL representation to the native one
    2782             :         // (since that's what zarr_v3_codec_sharding::FillWithNoData()
    2783             :         // expects
    2784           0 :         CPLError(CE_Warning, CPLE_AppDefined,
    2785             :                  "Zarr driver issue: gdalTypeIsApproxOfNative is not taken "
    2786             :                  "into account by codecs. Nodata will be assumed to be zero by "
    2787             :                  "sharding codec");
    2788             :     }
    2789        2301 :     else if (!abyNoData.empty() &&
    2790         989 :              (zarrDataType.nativeType == DtypeElt::NativeType::STRING_ASCII ||
    2791         984 :               zarrDataType.nativeType == DtypeElt::NativeType::STRING_UNICODE))
    2792             :     {
    2793             :         // Convert from GDAL representation (char* pointer) to native
    2794             :         // format (fixed-size null-padded buffer) for FillWithNoData()
    2795           6 :         char *pStr = nullptr;
    2796           6 :         memcpy(&pStr, abyNoData.data(), sizeof(pStr));
    2797           6 :         oInputArrayMetadata.abyNoData.resize(zarrDataType.nativeSize, 0);
    2798          12 :         if (pStr &&
    2799           6 :             zarrDataType.nativeType == DtypeElt::NativeType::STRING_ASCII)
    2800             :         {
    2801             :             const size_t nCopy =
    2802          15 :                 std::min(strlen(pStr), zarrDataType.nativeSize > 0
    2803           5 :                                            ? zarrDataType.nativeSize - 1
    2804           5 :                                            : static_cast<size_t>(0));
    2805           5 :             memcpy(oInputArrayMetadata.abyNoData.data(), pStr, nCopy);
    2806             :         }
    2807             :         // STRING_UNICODE non-empty fill would need UTF-8 to UCS4
    2808             :         // conversion; zero-fill is correct for the common "" case
    2809             :     }
    2810             :     else
    2811             :     {
    2812        1306 :         oInputArrayMetadata.abyNoData = abyNoData;
    2813             :     }
    2814        3730 :     for (auto &nSize : anOuterBlockSize)
    2815             :     {
    2816        2418 :         oInputArrayMetadata.anBlockSizes.push_back(static_cast<size_t>(nSize));
    2817             :     }
    2818        1312 :     oInputArrayMetadata.oElt = zarrDataType;
    2819        2624 :     auto poCodecs = std::make_unique<ZarrV3CodecSequence>(oInputArrayMetadata);
    2820        2624 :     ZarrArrayMetadata oOutputArrayMetadata;
    2821        1312 :     if (!poCodecs->InitFromJson(oCodecs, oOutputArrayMetadata))
    2822             :     {
    2823          18 :         return nullptr;
    2824             :     }
    2825        2588 :     std::vector<size_t> anOuterBlockSizeSizet;
    2826        3676 :     for (auto nVal : oOutputArrayMetadata.anBlockSizes)
    2827             :     {
    2828        2382 :         anOuterBlockSizeSizet.push_back(static_cast<size_t>(nVal));
    2829             :     }
    2830        1294 :     anInnerBlockSize.clear();
    2831        3676 :     for (size_t nVal : poCodecs->GetInnerMostBlockSize(anOuterBlockSizeSizet))
    2832             :     {
    2833        2382 :         anInnerBlockSize.push_back(nVal);
    2834             :     }
    2835        1294 :     return poCodecs;
    2836             : }
    2837             : 
    2838             : /************************************************************************/
    2839             : /*                       ZarrV3Array::SetCodecs()                       */
    2840             : /************************************************************************/
    2841             : 
    2842        1294 : void ZarrV3Array::SetCodecs(const CPLJSONArray &oJSONCodecs,
    2843             :                             std::unique_ptr<ZarrV3CodecSequence> &&poCodecs)
    2844             : {
    2845        1294 :     m_oJSONCodecs = oJSONCodecs;
    2846        1294 :     m_poCodecs = std::move(poCodecs);
    2847        1294 : }
    2848             : 
    2849             : /************************************************************************/
    2850             : /*                     ZarrV3Array::LoadOverviews()                     */
    2851             : /************************************************************************/
    2852             : 
    2853          99 : void ZarrV3Array::LoadOverviews() const
    2854             : {
    2855          99 :     if (m_bOverviewsLoaded)
    2856          51 :         return;
    2857          56 :     m_bOverviewsLoaded = true;
    2858             : 
    2859             :     // Cf https://github.com/zarr-conventions/multiscales
    2860             :     // and https://github.com/zarr-conventions/spatial
    2861             : 
    2862          56 :     const auto poRG = GetRootGroup();
    2863          56 :     if (!poRG)
    2864             :     {
    2865           1 :         CPLError(CE_Warning, CPLE_AppDefined,
    2866             :                  "LoadOverviews(): cannot access root group");
    2867           1 :         return;
    2868             :     }
    2869             : 
    2870          55 :     auto poGroup = GetParentGroup();
    2871          55 :     if (!poGroup)
    2872             :     {
    2873           0 :         CPLDebugOnly(ZARR_DEBUG_KEY,
    2874             :                      "LoadOverviews(): cannot access parent group");
    2875           0 :         return;
    2876             :     }
    2877             : 
    2878             :     // Look for "zarr_conventions" and "multiscales" attributes in our
    2879             :     // immediate parent, or in our grandparent if not found.
    2880         110 :     auto poAttrZarrConventions = poGroup->GetAttribute("zarr_conventions");
    2881         110 :     auto poAttrMultiscales = poGroup->GetAttribute("multiscales");
    2882          55 :     if (!poAttrZarrConventions || !poAttrMultiscales)
    2883             :     {
    2884          43 :         poGroup = poGroup->GetParentGroup();
    2885          43 :         if (poGroup)
    2886             :         {
    2887          43 :             poAttrZarrConventions = poGroup->GetAttribute("zarr_conventions");
    2888          43 :             poAttrMultiscales = poGroup->GetAttribute("multiscales");
    2889             :         }
    2890          43 :         if (!poAttrZarrConventions || !poAttrMultiscales)
    2891             :         {
    2892           1 :             return;
    2893             :         }
    2894             :     }
    2895             : 
    2896          54 :     const char *pszZarrConventions = poAttrZarrConventions->ReadAsString();
    2897          54 :     const char *pszMultiscales = poAttrMultiscales->ReadAsString();
    2898          54 :     if (!pszZarrConventions || !pszMultiscales)
    2899           0 :         return;
    2900             : 
    2901          54 :     CPLJSONDocument oDoc;
    2902          54 :     if (!oDoc.LoadMemory(pszZarrConventions))
    2903           0 :         return;
    2904          54 :     const auto oZarrConventions = oDoc.GetRoot();
    2905             : 
    2906          54 :     if (!oDoc.LoadMemory(pszMultiscales))
    2907           0 :         return;
    2908          54 :     const auto oMultiscales = oDoc.GetRoot();
    2909             : 
    2910          54 :     if (!oZarrConventions.IsValid() ||
    2911          54 :         oZarrConventions.GetType() != CPLJSONObject::Type::Array ||
    2912         162 :         !oMultiscales.IsValid() ||
    2913          54 :         oMultiscales.GetType() != CPLJSONObject::Type::Object)
    2914             :     {
    2915           0 :         return;
    2916             :     }
    2917             : 
    2918          54 :     const auto oZarrConventionsArray = oZarrConventions.ToArray();
    2919          54 :     const auto hasMultiscalesUUIDLambda = [](const CPLJSONObject &obj)
    2920          54 :     { return obj.GetString("uuid") == ZARR_MULTISCALES_UUID; };
    2921             :     const bool bFoundMultiScalesUUID =
    2922         108 :         std::find_if(oZarrConventionsArray.begin(), oZarrConventionsArray.end(),
    2923         162 :                      hasMultiscalesUUIDLambda) != oZarrConventionsArray.end();
    2924          54 :     if (!bFoundMultiScalesUUID)
    2925           0 :         return;
    2926             : 
    2927          68 :     const auto hasSpatialUUIDLambda = [](const CPLJSONObject &obj)
    2928             :     {
    2929          68 :         constexpr const char *SPATIAL_UUID =
    2930             :             "689b58e2-cf7b-45e0-9fff-9cfc0883d6b4";
    2931          68 :         return obj.GetString("uuid") == SPATIAL_UUID;
    2932             :     };
    2933             :     const bool bFoundSpatialUUID =
    2934         108 :         std::find_if(oZarrConventionsArray.begin(), oZarrConventionsArray.end(),
    2935         162 :                      hasSpatialUUIDLambda) != oZarrConventionsArray.end();
    2936             : 
    2937         108 :     const auto oLayout = oMultiscales["layout"];
    2938          54 :     if (!oLayout.IsValid() && oLayout.GetType() != CPLJSONObject::Type::Array)
    2939             :     {
    2940           1 :         CPLError(CE_Warning, CPLE_AppDefined,
    2941             :                  "layout not found in multiscales");
    2942           1 :         return;
    2943             :     }
    2944             : 
    2945             :     // is pixel-is-area ?
    2946         106 :     auto poSpatialRegistration = poGroup->GetAttribute("spatial:registration");
    2947             :     const char *pszSpatialRegistration =
    2948          53 :         poSpatialRegistration ? poSpatialRegistration->ReadAsString() : nullptr;
    2949          53 :     const bool bHasExplicitPixelSpatialRegistration =
    2950          67 :         bFoundSpatialUUID && pszSpatialRegistration &&
    2951          14 :         strcmp(pszSpatialRegistration, "pixel") == 0;
    2952             : 
    2953          53 :     std::vector<std::string> aosSpatialDimensions;
    2954          53 :     std::set<std::string> oSetSpatialDimensionNames;
    2955         106 :     auto poSpatialDimensions = poGroup->GetAttribute("spatial:dimensions");
    2956          53 :     if (bFoundSpatialUUID && poSpatialDimensions)
    2957             :     {
    2958          14 :         aosSpatialDimensions = poSpatialDimensions->ReadAsStringArray();
    2959          42 :         for (const auto &osDimName : aosSpatialDimensions)
    2960             :         {
    2961          28 :             oSetSpatialDimensionNames.insert(osDimName);
    2962             :         }
    2963             :     }
    2964             : 
    2965             :     // Multiscales convention: asset/derived_from paths are relative to
    2966             :     // the group holding the convention metadata, not the store root.
    2967          53 :     const std::string osGroupPrefix = poGroup->GetFullName().back() == '/'
    2968          51 :                                           ? poGroup->GetFullName()
    2969         104 :                                           : poGroup->GetFullName() + '/';
    2970             :     const auto resolveAssetPath =
    2971         240 :         [&osGroupPrefix](const std::string &osRelative) -> std::string
    2972         240 :     { return osGroupPrefix + osRelative; };
    2973             : 
    2974             :     // Check whether this multiscales describes our array's pyramid.
    2975             :     // The first layout entry is the base (full-resolution) level; its
    2976             :     // "asset" field identifies the target array.  If it refers to a
    2977             :     // different array, the entire layout is irrelevant to us.
    2978             :     {
    2979          53 :         const auto oFirstItem = oLayout.ToArray()[0];
    2980         106 :         const std::string osBaseAsset = oFirstItem.GetString("asset");
    2981          53 :         if (!osBaseAsset.empty())
    2982             :         {
    2983           0 :             std::shared_ptr<GDALGroup> poBaseGroup;
    2984             :             {
    2985          52 :                 CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    2986             :                 poBaseGroup =
    2987          52 :                     poRG->OpenGroupFromFullname(resolveAssetPath(osBaseAsset));
    2988             :             }
    2989          52 :             if (poBaseGroup)
    2990             :             {
    2991             :                 // Group-based layout (e.g. OME-Zarr "0", "1", ...):
    2992             :                 // skip if the base group has no array with our name.
    2993          33 :                 CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    2994          33 :                 if (!poBaseGroup->OpenMDArray(GetName()))
    2995           0 :                     return;
    2996             :             }
    2997             :             else
    2998             :             {
    2999             :                 // Extract array name from path (e.g. "level0/ar" -> "ar").
    3000          19 :                 const auto nSlash = osBaseAsset.rfind('/');
    3001             :                 const std::string osArrayName =
    3002             :                     nSlash != std::string::npos ? osBaseAsset.substr(nSlash + 1)
    3003          19 :                                                 : osBaseAsset;
    3004          19 :                 if (osArrayName != GetName())
    3005           5 :                     return;
    3006             :             }
    3007             :         }
    3008             :     }
    3009             : 
    3010         183 :     for (const auto &oLayoutItem : oLayout.ToArray())
    3011             :     {
    3012         270 :         const std::string osAsset = oLayoutItem.GetString("asset");
    3013         135 :         if (osAsset.empty())
    3014             :         {
    3015           1 :             CPLError(CE_Warning, CPLE_AppDefined,
    3016             :                      "multiscales.layout[].asset not found");
    3017           1 :             continue;
    3018             :         }
    3019             : 
    3020             :         // Resolve "asset" to a MDArray
    3021           0 :         std::shared_ptr<GDALGroup> poAssetGroup;
    3022             :         {
    3023         134 :             CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    3024             :             poAssetGroup =
    3025         134 :                 poRG->OpenGroupFromFullname(resolveAssetPath(osAsset));
    3026             :         }
    3027           0 :         std::shared_ptr<GDALMDArray> poAssetArray;
    3028         134 :         if (poAssetGroup)
    3029             :         {
    3030         110 :             poAssetArray = poAssetGroup->OpenMDArray(GetName());
    3031             :         }
    3032          24 :         else if (osAsset.find('/') == std::string::npos)
    3033             :         {
    3034           9 :             poAssetArray = poGroup->OpenMDArray(osAsset);
    3035           9 :             if (!poAssetArray)
    3036             :             {
    3037           0 :                 CPLError(CE_Warning, CPLE_AppDefined,
    3038             :                          "multiscales.layout[].asset=%s ignored, because it is "
    3039             :                          "not a valid group or array name",
    3040             :                          osAsset.c_str());
    3041           0 :                 continue;
    3042             :             }
    3043             :         }
    3044             :         else
    3045             :         {
    3046             :             poAssetArray =
    3047          15 :                 poRG->OpenMDArrayFromFullname(resolveAssetPath(osAsset));
    3048          15 :             if (poAssetArray && poAssetArray->GetName() != GetName())
    3049             :             {
    3050           0 :                 continue;
    3051             :             }
    3052             :         }
    3053         134 :         if (!poAssetArray)
    3054             :         {
    3055          10 :             continue;
    3056             :         }
    3057         124 :         if (poAssetArray->GetDimensionCount() != GetDimensionCount())
    3058             :         {
    3059           3 :             CPLError(
    3060             :                 CE_Warning, CPLE_AppDefined,
    3061             :                 "multiscales.layout[].asset=%s (%s) ignored, because it  has "
    3062             :                 "not the same dimension count as %s (%s)",
    3063           1 :                 osAsset.c_str(), poAssetArray->GetFullName().c_str(),
    3064           2 :                 GetName().c_str(), GetFullName().c_str());
    3065           1 :             continue;
    3066             :         }
    3067         123 :         if (poAssetArray->GetDataType() != GetDataType())
    3068             :         {
    3069           3 :             CPLError(
    3070             :                 CE_Warning, CPLE_AppDefined,
    3071             :                 "multiscales.layout[].asset=%s (%s) ignored, because it has "
    3072             :                 "not the same data type as %s (%s)",
    3073           1 :                 osAsset.c_str(), poAssetArray->GetFullName().c_str(),
    3074           2 :                 GetName().c_str(), GetFullName().c_str());
    3075           1 :             continue;
    3076             :         }
    3077             : 
    3078         122 :         bool bAssetIsDownsampledOfThis = false;
    3079         239 :         for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
    3080             :         {
    3081         358 :             if (poAssetArray->GetDimensions()[iDim]->GetSize() <
    3082         179 :                 GetDimensions()[iDim]->GetSize())
    3083             :             {
    3084          62 :                 bAssetIsDownsampledOfThis = true;
    3085          62 :                 break;
    3086             :             }
    3087             :         }
    3088         122 :         if (!bAssetIsDownsampledOfThis)
    3089             :         {
    3090             :             // not an error
    3091          60 :             continue;
    3092             :         }
    3093             : 
    3094             :         // Inspect dimensions of the asset
    3095          62 :         std::map<std::string, size_t> oMapAssetDimNameToIdx;
    3096          62 :         const auto &apoAssetDims = poAssetArray->GetDimensions();
    3097          62 :         size_t nCountSpatialDimsFoundInAsset = 0;
    3098         184 :         for (const auto &[idx, poDim] : cpl::enumerate(apoAssetDims))
    3099             :         {
    3100         122 :             oMapAssetDimNameToIdx[poDim->GetName()] = idx;
    3101         122 :             if (cpl::contains(oSetSpatialDimensionNames, poDim->GetName()))
    3102          43 :                 ++nCountSpatialDimsFoundInAsset;
    3103             :         }
    3104             :         const bool bAssetHasAllSpatialDims =
    3105          62 :             (nCountSpatialDimsFoundInAsset == aosSpatialDimensions.size());
    3106             : 
    3107             :         // Consistency checks on "derived_from" and "transform"
    3108         124 :         const auto oDerivedFrom = oLayoutItem["derived_from"];
    3109         124 :         const auto oTransform = oLayoutItem["transform"];
    3110          97 :         if (oDerivedFrom.IsValid() && oTransform.IsValid() &&
    3111         132 :             oDerivedFrom.GetType() == CPLJSONObject::Type::String &&
    3112          35 :             oTransform.GetType() == CPLJSONObject::Type::Object)
    3113             :         {
    3114          70 :             const std::string osDerivedFrom = oDerivedFrom.ToString();
    3115             :             // Resolve "derived_from" to a MDArray
    3116           0 :             std::shared_ptr<GDALGroup> poDerivedFromGroup;
    3117             :             {
    3118          35 :                 CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    3119         105 :                 poDerivedFromGroup = poRG->OpenGroupFromFullname(
    3120         105 :                     resolveAssetPath(osDerivedFrom));
    3121             :             }
    3122           0 :             std::shared_ptr<GDALMDArray> poDerivedFromArray;
    3123          35 :             if (poDerivedFromGroup)
    3124             :             {
    3125          21 :                 poDerivedFromArray = poDerivedFromGroup->OpenMDArray(GetName());
    3126             :             }
    3127          14 :             else if (osDerivedFrom.find('/') == std::string::npos)
    3128             :             {
    3129          10 :                 poDerivedFromArray = poGroup->OpenMDArray(osDerivedFrom);
    3130          10 :                 if (!poDerivedFromArray)
    3131             :                 {
    3132           1 :                     CPLError(CE_Warning, CPLE_AppDefined,
    3133             :                              "multiscales.layout[].asset=%s refers to "
    3134             :                              "derived_from=%s which does not exist",
    3135             :                              osAsset.c_str(), osDerivedFrom.c_str());
    3136           1 :                     poDerivedFromArray.reset();
    3137             :                 }
    3138             :             }
    3139             :             else
    3140             :             {
    3141          12 :                 poDerivedFromArray = poRG->OpenMDArrayFromFullname(
    3142          12 :                     resolveAssetPath(osDerivedFrom));
    3143             :             }
    3144          35 :             if (poDerivedFromArray && bAssetHasAllSpatialDims)
    3145             :             {
    3146          33 :                 if (poDerivedFromArray->GetDimensionCount() !=
    3147          33 :                     GetDimensionCount())
    3148             :                 {
    3149           1 :                     CPLError(CE_Warning, CPLE_AppDefined,
    3150             :                              "multiscales.layout[].asset=%s refers to "
    3151             :                              "derived_from=%s that does not have the expected "
    3152             :                              "number of dimensions. Ignoring that asset",
    3153             :                              osAsset.c_str(), osDerivedFrom.c_str());
    3154           3 :                     continue;
    3155             :                 }
    3156             : 
    3157          64 :                 const auto oScale = oTransform["scale"];
    3158          32 :                 if (oScale.GetType() == CPLJSONObject::Type::Array &&
    3159             :                     bHasExplicitPixelSpatialRegistration)
    3160             :                 {
    3161          10 :                     const auto oScaleArray = oScale.ToArray();
    3162          10 :                     if (oScaleArray.size() != GetDimensionCount())
    3163             :                     {
    3164             : 
    3165           1 :                         CPLError(CE_Warning, CPLE_AppDefined,
    3166             :                                  "multiscales.layout[].asset=%s has a "
    3167             :                                  "transform.scale array with an unexpected "
    3168             :                                  "number of values. Ignoring the asset",
    3169             :                                  osAsset.c_str());
    3170           1 :                         continue;
    3171             :                     }
    3172             : 
    3173          27 :                     for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
    3174             :                     {
    3175          18 :                         const double dfScale = oScaleArray[iDim].ToDouble();
    3176             :                         const double dfExpectedScale =
    3177          18 :                             static_cast<double>(
    3178          18 :                                 poDerivedFromArray->GetDimensions()[iDim]
    3179          18 :                                     ->GetSize()) /
    3180          18 :                             static_cast<double>(
    3181          18 :                                 poAssetArray->GetDimensions()[iDim]->GetSize());
    3182          18 :                         constexpr double EPSILON = 1e-3;
    3183          18 :                         if (std::fabs(dfScale - dfExpectedScale) >
    3184          18 :                             EPSILON * dfExpectedScale)
    3185             :                         {
    3186           2 :                             CPLError(CE_Warning, CPLE_AppDefined,
    3187             :                                      "multiscales.layout[].asset=%s has a "
    3188             :                                      "transform.scale[%d]=%f value whereas %f "
    3189             :                                      "was expected. "
    3190             :                                      "Assuming that later value as the scale.",
    3191             :                                      osAsset.c_str(), static_cast<int>(iDim),
    3192             :                                      dfScale, dfExpectedScale);
    3193             :                         }
    3194             :                     }
    3195             :                 }
    3196             : 
    3197          62 :                 const auto oTranslation = oTransform["translation"];
    3198          31 :                 if (oTranslation.GetType() == CPLJSONObject::Type::Array &&
    3199             :                     bHasExplicitPixelSpatialRegistration)
    3200             :                 {
    3201           9 :                     const auto oTranslationArray = oTranslation.ToArray();
    3202           9 :                     if (oTranslationArray.size() != GetDimensionCount())
    3203             :                     {
    3204           1 :                         CPLError(CE_Warning, CPLE_AppDefined,
    3205             :                                  "multiscales.layout[].asset=%s has a "
    3206             :                                  "transform.translation array with an "
    3207             :                                  "unexpected number of values. "
    3208             :                                  "Ignoring the asset",
    3209             :                                  osAsset.c_str());
    3210           1 :                         continue;
    3211             :                     }
    3212             : 
    3213          24 :                     for (size_t iDim = 0; iDim < GetDimensionCount(); ++iDim)
    3214             :                     {
    3215             :                         const double dfOffset =
    3216          16 :                             oTranslationArray[iDim].ToDouble();
    3217          16 :                         if (dfOffset != 0)
    3218             :                         {
    3219           1 :                             CPLError(CE_Warning, CPLE_AppDefined,
    3220             :                                      "multiscales.layout[].asset=%s has a "
    3221             :                                      "transform.translation[%d]=%f value. "
    3222             :                                      "Ignoring that offset.",
    3223             :                                      osAsset.c_str(), static_cast<int>(iDim),
    3224             :                                      dfOffset);
    3225             :                         }
    3226             :                     }
    3227             :                 }
    3228             :             }
    3229             :         }
    3230             : 
    3231          59 :         if (bFoundSpatialUUID && bAssetHasAllSpatialDims)
    3232             :         {
    3233          38 :             const auto oSpatialShape = oLayoutItem["spatial:shape"];
    3234          19 :             if (oSpatialShape.IsValid())
    3235             :             {
    3236          19 :                 if (oSpatialShape.GetType() != CPLJSONObject::Type::Array)
    3237             :                 {
    3238           1 :                     CPLError(
    3239             :                         CE_Warning, CPLE_AppDefined,
    3240             :                         "multiscales.layout[].asset=%s ignored, because its "
    3241             :                         "spatial:shape property is not an array",
    3242             :                         osAsset.c_str());
    3243           3 :                     continue;
    3244             :                 }
    3245          18 :                 const auto oSpatialShapeArray = oSpatialShape.ToArray();
    3246          18 :                 if (oSpatialShapeArray.size() != aosSpatialDimensions.size())
    3247             :                 {
    3248           1 :                     CPLError(
    3249             :                         CE_Warning, CPLE_AppDefined,
    3250             :                         "multiscales.layout[].asset=%s ignored, because its "
    3251             :                         "spatial:shape property has not the expected number "
    3252             :                         "of values",
    3253             :                         osAsset.c_str());
    3254           1 :                     continue;
    3255             :                 }
    3256             : 
    3257          17 :                 bool bSkip = false;
    3258          68 :                 for (const auto &[idx, oShapeVal] :
    3259          85 :                      cpl::enumerate(oSpatialShapeArray))
    3260             :                 {
    3261             :                     const auto oIter =
    3262          34 :                         oMapAssetDimNameToIdx.find(aosSpatialDimensions[idx]);
    3263          34 :                     if (oIter != oMapAssetDimNameToIdx.end())
    3264             :                     {
    3265          68 :                         const auto poDim = apoAssetDims[oIter->second];
    3266          34 :                         if (poDim->GetSize() !=
    3267          34 :                             static_cast<uint64_t>(oShapeVal.ToLong()))
    3268             :                         {
    3269           1 :                             bSkip = true;
    3270           1 :                             CPLError(CE_Warning, CPLE_AppDefined,
    3271             :                                      "multiscales.layout[].asset=%s ignored, "
    3272             :                                      "because its "
    3273             :                                      "spatial:shape[%d] value is %" PRIu64
    3274             :                                      " whereas %" PRIu64 " was expected.",
    3275           1 :                                      osAsset.c_str(), static_cast<int>(idx),
    3276           1 :                                      static_cast<uint64_t>(oShapeVal.ToLong()),
    3277           1 :                                      static_cast<uint64_t>(poDim->GetSize()));
    3278             :                         }
    3279             :                     }
    3280             :                 }
    3281          17 :                 if (bSkip)
    3282           1 :                     continue;
    3283             :             }
    3284             :         }
    3285             : 
    3286          56 :         m_apoOverviews.push_back(std::move(poAssetArray));
    3287             :     }
    3288             : }
    3289             : 
    3290             : /************************************************************************/
    3291             : /*                   ZarrV3Array::GetOverviewCount()                    */
    3292             : /************************************************************************/
    3293             : 
    3294          99 : int ZarrV3Array::GetOverviewCount() const
    3295             : {
    3296          99 :     LoadOverviews();
    3297          99 :     return static_cast<int>(m_apoOverviews.size());
    3298             : }
    3299             : 
    3300             : /************************************************************************/
    3301             : /*                      ZarrV3Array::GetOverview()                      */
    3302             : /************************************************************************/
    3303             : 
    3304          54 : std::shared_ptr<GDALMDArray> ZarrV3Array::GetOverview(int idx) const
    3305             : {
    3306          54 :     if (idx < 0 || idx >= GetOverviewCount())
    3307          12 :         return nullptr;
    3308          42 :     return m_apoOverviews[idx];
    3309             : }
    3310             : 
    3311             : /************************************************************************/
    3312             : /*         ZarrV3Array::ReconstructCreationOptionsFromCodecs()          */
    3313             : /************************************************************************/
    3314             : 
    3315             : // When an array is opened from disk (LoadArray), m_aosCreationOptions is
    3316             : // empty because SetCreationOptions() is only called during CreateMDArray().
    3317             : // BuildOverviews() needs the creation options so that overview arrays
    3318             : // inherit the same codec (compression, sharding).  This method reverse-maps
    3319             : // the stored codec JSON back to creation option key/value pairs.
    3320             : 
    3321           9 : void ZarrV3Array::ReconstructCreationOptionsFromCodecs()
    3322             : {
    3323           9 :     if (!m_poCodecs || m_aosCreationOptions.FetchNameValue("COMPRESS"))
    3324           1 :         return;
    3325             : 
    3326          16 :     CPLJSONArray oCodecArray = m_poCodecs->GetJSon().ToArray();
    3327             : 
    3328             :     // Detect sharding: if the sole top-level codec is sharding_indexed,
    3329             :     // extract SHARD_CHUNK_SHAPE and use the inner codecs for compression.
    3330          15 :     for (int i = 0; i < oCodecArray.Size(); ++i)
    3331             :     {
    3332           8 :         const auto oCodec = oCodecArray[i];
    3333           8 :         if (oCodec.GetString("name") == "sharding_indexed")
    3334             :         {
    3335           3 :             const auto oConfig = oCodec["configuration"];
    3336             : 
    3337             :             // Inner chunk shape
    3338           3 :             const auto oChunkShape = oConfig.GetArray("chunk_shape");
    3339           1 :             if (oChunkShape.IsValid() && oChunkShape.Size() > 0)
    3340             :             {
    3341           2 :                 std::string osShape;
    3342           3 :                 for (int j = 0; j < oChunkShape.Size(); ++j)
    3343             :                 {
    3344           2 :                     if (!osShape.empty())
    3345           1 :                         osShape += ',';
    3346             :                     osShape += CPLSPrintf(
    3347             :                         CPL_FRMT_GUIB,
    3348           2 :                         static_cast<GUIntBig>(oChunkShape[j].ToLong()));
    3349             :                 }
    3350             :                 m_aosCreationOptions.SetNameValue("SHARD_CHUNK_SHAPE",
    3351           1 :                                                   osShape.c_str());
    3352             :             }
    3353             : 
    3354             :             // Use inner codecs for compression detection
    3355           1 :             oCodecArray = oConfig.GetArray("codecs");
    3356           1 :             break;
    3357             :         }
    3358             :     }
    3359             : 
    3360             :     // Scan codecs for compression algorithm
    3361          17 :     for (int i = 0; i < oCodecArray.Size(); ++i)
    3362             :     {
    3363          18 :         const auto oCodec = oCodecArray[i];
    3364          27 :         const auto osName = oCodec.GetString("name");
    3365          27 :         const auto oConfig = oCodec["configuration"];
    3366             : 
    3367           9 :         if (osName == "gzip")
    3368             :         {
    3369           0 :             m_aosCreationOptions.SetNameValue("COMPRESS", "GZIP");
    3370           0 :             if (oConfig.IsValid())
    3371             :             {
    3372             :                 m_aosCreationOptions.SetNameValue(
    3373             :                     "GZIP_LEVEL",
    3374           0 :                     CPLSPrintf("%d", oConfig.GetInteger("level")));
    3375             :             }
    3376             :         }
    3377           9 :         else if (osName == "zstd")
    3378             :         {
    3379           1 :             m_aosCreationOptions.SetNameValue("COMPRESS", "ZSTD");
    3380           1 :             if (oConfig.IsValid())
    3381             :             {
    3382             :                 m_aosCreationOptions.SetNameValue(
    3383             :                     "ZSTD_LEVEL",
    3384           1 :                     CPLSPrintf("%d", oConfig.GetInteger("level")));
    3385           1 :                 if (oConfig.GetBool("checksum"))
    3386           0 :                     m_aosCreationOptions.SetNameValue("ZSTD_CHECKSUM", "YES");
    3387             :             }
    3388             :         }
    3389           8 :         else if (osName == "blosc")
    3390             :         {
    3391           0 :             m_aosCreationOptions.SetNameValue("COMPRESS", "BLOSC");
    3392           0 :             if (oConfig.IsValid())
    3393             :             {
    3394           0 :                 const auto osCName = oConfig.GetString("cname");
    3395           0 :                 if (!osCName.empty())
    3396             :                     m_aosCreationOptions.SetNameValue("BLOSC_CNAME",
    3397           0 :                                                       osCName.c_str());
    3398             :                 m_aosCreationOptions.SetNameValue(
    3399             :                     "BLOSC_CLEVEL",
    3400           0 :                     CPLSPrintf("%d", oConfig.GetInteger("clevel")));
    3401           0 :                 const auto osShuffle = oConfig.GetString("shuffle");
    3402           0 :                 if (osShuffle == "noshuffle")
    3403           0 :                     m_aosCreationOptions.SetNameValue("BLOSC_SHUFFLE", "NONE");
    3404           0 :                 else if (osShuffle == "bitshuffle")
    3405           0 :                     m_aosCreationOptions.SetNameValue("BLOSC_SHUFFLE", "BIT");
    3406             :                 else
    3407           0 :                     m_aosCreationOptions.SetNameValue("BLOSC_SHUFFLE", "BYTE");
    3408             :             }
    3409             :         }
    3410             :     }
    3411             : }
    3412             : 
    3413             : /************************************************************************/
    3414             : /*                    ZarrV3Array::BuildOverviews()                     */
    3415             : /************************************************************************/
    3416             : 
    3417          14 : CPLErr ZarrV3Array::BuildOverviews(const char *pszResampling, int nOverviews,
    3418             :                                    const int *panOverviewList,
    3419             :                                    GDALProgressFunc pfnProgress,
    3420             :                                    void *pProgressData,
    3421             :                                    CSLConstList /* papszOptions */)
    3422             : {
    3423          14 :     const size_t nDimCount = GetDimensionCount();
    3424          14 :     if (nDimCount < 2)
    3425             :     {
    3426           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    3427             :                  "BuildOverviews() requires at least 2 dimensions");
    3428           1 :         return CE_Failure;
    3429             :     }
    3430             : 
    3431          13 :     if (!m_bUpdatable)
    3432             :     {
    3433           1 :         CPLError(CE_Failure, CPLE_AppDefined,
    3434             :                  "Dataset not open in update mode");
    3435           1 :         return CE_Failure;
    3436             :     }
    3437             : 
    3438             :     auto poParentGroup =
    3439          24 :         std::static_pointer_cast<ZarrV3Group>(GetParentGroup());
    3440          12 :     if (!poParentGroup)
    3441             :     {
    3442           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Cannot access parent group");
    3443           0 :         return CE_Failure;
    3444             :     }
    3445             : 
    3446          12 :     if (!pfnProgress)
    3447          11 :         pfnProgress = GDALDummyProgress;
    3448             : 
    3449             :     // Identify spatial dimensions via GDALDimension::GetType().
    3450             :     // Fall back to last two dimensions (Y, X) if types are not set.
    3451          12 :     const auto &apoSrcDims = GetDimensions();
    3452          12 :     size_t iYDim = nDimCount - 2;
    3453          12 :     size_t iXDim = nDimCount - 1;
    3454             : 
    3455          37 :     for (size_t i = 0; i < nDimCount; ++i)
    3456             :     {
    3457          25 :         if (apoSrcDims[i]->GetType() == GDAL_DIM_TYPE_HORIZONTAL_X)
    3458           2 :             iXDim = i;
    3459          23 :         else if (apoSrcDims[i]->GetType() == GDAL_DIM_TYPE_HORIZONTAL_Y)
    3460           2 :             iYDim = i;
    3461             :     }
    3462             : 
    3463          12 :     if (iXDim == iYDim)
    3464             :     {
    3465           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    3466             :                  "Cannot identify two distinct spatial dimensions. "
    3467             :                  "Set dimension types (HORIZONTAL_X / HORIZONTAL_Y) "
    3468             :                  "or ensure the array has at least 2 dimensions.");
    3469           0 :         return CE_Failure;
    3470             :     }
    3471             : 
    3472             :     // Delete existing overview groups (ovr_*) for idempotent rebuild.
    3473             :     // Also handles nOverviews==0 ("clear overviews").
    3474          14 :     for (const auto &osName : poParentGroup->GetGroupNames())
    3475             :     {
    3476           4 :         if (STARTS_WITH(osName.c_str(), "ovr_") &&
    3477           2 :             !poParentGroup->DeleteGroup(osName))
    3478             :         {
    3479           0 :             CPLError(CE_Failure, CPLE_AppDefined,
    3480             :                      "Cannot delete existing overview group '%s'",
    3481             :                      osName.c_str());
    3482           0 :             return CE_Failure;
    3483             :         }
    3484             :     }
    3485             : 
    3486          12 :     if (nOverviews == 0)
    3487             :     {
    3488           1 :         poParentGroup->GenerateMultiscalesMetadata(nullptr);
    3489           1 :         m_bOverviewsLoaded = false;
    3490           1 :         m_apoOverviews.clear();
    3491           1 :         return CE_None;
    3492             :     }
    3493             : 
    3494          11 :     if (nOverviews < 0 || !panOverviewList)
    3495             :     {
    3496           0 :         CPLError(CE_Failure, CPLE_IllegalArg, "Invalid overview list");
    3497           0 :         return CE_Failure;
    3498             :     }
    3499             : 
    3500          11 :     if (!pszResampling || pszResampling[0] == '\0')
    3501           0 :         pszResampling = "NEAREST";
    3502             : 
    3503             :     // Sort and deduplicate factors for sequential resampling chain.
    3504          22 :     std::vector<int> anFactors(panOverviewList, panOverviewList + nOverviews);
    3505          11 :     std::sort(anFactors.begin(), anFactors.end());
    3506          11 :     anFactors.erase(std::unique(anFactors.begin(), anFactors.end()),
    3507          22 :                     anFactors.end());
    3508          22 :     for (const int nFactor : anFactors)
    3509             :     {
    3510          13 :         if (nFactor < 2)
    3511             :         {
    3512           2 :             CPLError(CE_Failure, CPLE_IllegalArg,
    3513             :                      "Overview factor %d is invalid (must be >= 2)", nFactor);
    3514           2 :             return CE_Failure;
    3515             :         }
    3516             :     }
    3517             : 
    3518             :     // Ensure creation options are populated (they are empty when the array
    3519             :     // was opened from disk rather than freshly created).
    3520           9 :     ReconstructCreationOptionsFromCodecs();
    3521             : 
    3522             :     // Inherit creation options from source array (codec settings, etc.).
    3523             :     // Only override BLOCKSIZE and SHARD_CHUNK_SHAPE per level.
    3524          18 :     CPLStringList aosCreateOptions(m_aosCreationOptions);
    3525             : 
    3526           9 :     const std::string &osArrayName = GetName();
    3527           9 :     const void *pRawNoData = GetRawNoDataValue();
    3528             : 
    3529             :     // Build each level sequentially: 2x from base, 4x from 2x, etc.
    3530             :     // poChainSource starts as the base array and advances to each
    3531             :     // newly created overview so each level resamples from the previous.
    3532             :     std::shared_ptr<GDALMDArray> poChainSource =
    3533          18 :         std::dynamic_pointer_cast<GDALMDArray>(m_pSelf.lock());
    3534           9 :     if (!poChainSource)
    3535             :     {
    3536           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    3537             :                  "Cannot obtain shared_ptr to self");
    3538           0 :         return CE_Failure;
    3539             :     }
    3540             : 
    3541             :     // Pre-compute total output pixels for pixel-weighted progress.
    3542           9 :     double dfTotalPixels = 0.0;
    3543          20 :     for (const int nF : anFactors)
    3544             :     {
    3545          33 :         dfTotalPixels += static_cast<double>(
    3546          22 :                              DIV_ROUND_UP(apoSrcDims[iYDim]->GetSize(), nF)) *
    3547          11 :                          DIV_ROUND_UP(apoSrcDims[iXDim]->GetSize(), nF);
    3548             :     }
    3549           9 :     double dfPixelsProcessed = 0.0;
    3550             : 
    3551           9 :     const int nFactorCount = static_cast<int>(anFactors.size());
    3552          20 :     for (int iOvr = 0; iOvr < nFactorCount; ++iOvr)
    3553             :     {
    3554          11 :         const int nFactor = anFactors[iOvr];
    3555             : 
    3556             :         // Create sibling group for this overview level.
    3557          11 :         const std::string osGroupName = CPLSPrintf("ovr_%dx", nFactor);
    3558          11 :         auto poOvrGroup = poParentGroup->CreateGroup(osGroupName);
    3559          11 :         if (!poOvrGroup)
    3560             :         {
    3561           0 :             CPLError(CE_Failure, CPLE_AppDefined, "Cannot create group '%s'",
    3562             :                      osGroupName.c_str());
    3563           0 :             return CE_Failure;
    3564             :         }
    3565             : 
    3566             :         // Create dimensions: downsample spatial, preserve non-spatial.
    3567          11 :         std::vector<std::shared_ptr<GDALDimension>> aoOvrDims;
    3568          11 :         std::string osBlockSize;
    3569          34 :         for (size_t i = 0; i < nDimCount; ++i)
    3570             :         {
    3571          23 :             const bool bSpatial = (i == iYDim || i == iXDim);
    3572          23 :             const GUInt64 nSrcSize = apoSrcDims[i]->GetSize();
    3573          23 :             const GUInt64 nOvrSize =
    3574          23 :                 bSpatial ? DIV_ROUND_UP(nSrcSize, nFactor) : nSrcSize;
    3575             : 
    3576          23 :             auto poDim = poOvrGroup->CreateDimension(
    3577          46 :                 apoSrcDims[i]->GetName(), apoSrcDims[i]->GetType(),
    3578          69 :                 apoSrcDims[i]->GetDirection(), nOvrSize);
    3579          23 :             if (!poDim)
    3580             :             {
    3581           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    3582             :                          "Cannot create dimension '%s' in group '%s'",
    3583           0 :                          apoSrcDims[i]->GetName().c_str(), osGroupName.c_str());
    3584           0 :                 return CE_Failure;
    3585             :             }
    3586          23 :             aoOvrDims.push_back(std::move(poDim));
    3587             : 
    3588             :             // Block size: inherit from source, cap to overview dim size.
    3589          23 :             const GUInt64 nBlock = std::min(m_anOuterBlockSize[i], nOvrSize);
    3590          23 :             if (!osBlockSize.empty())
    3591          12 :                 osBlockSize += ',';
    3592             :             osBlockSize +=
    3593          23 :                 CPLSPrintf(CPL_FRMT_GUIB, static_cast<GUIntBig>(nBlock));
    3594             : 
    3595             :             // Build 1D coordinate array for spatial dimensions.
    3596          23 :             if (bSpatial)
    3597             :             {
    3598          22 :                 auto poSrcVar = apoSrcDims[i]->GetIndexingVariable();
    3599          22 :                 if (poSrcVar && poSrcVar->GetDimensionCount() == 1)
    3600             :                 {
    3601           4 :                     if (nOvrSize > 100 * 1000 * 1000)
    3602             :                     {
    3603           0 :                         CPLError(CE_Failure, CPLE_AppDefined,
    3604             :                                  "Overview dimension too large for "
    3605             :                                  "coordinate array");
    3606           0 :                         return CE_Failure;
    3607             :                     }
    3608           4 :                     const size_t nOvrCount = static_cast<size_t>(nOvrSize);
    3609           4 :                     auto poCoordArray = poOvrGroup->CreateMDArray(
    3610           4 :                         poSrcVar->GetName(), {aoOvrDims.back()},
    3611          16 :                         poSrcVar->GetDataType());
    3612           4 :                     if (poCoordArray)
    3613             :                     {
    3614           4 :                         std::vector<double> adfValues;
    3615             :                         try
    3616             :                         {
    3617           4 :                             adfValues.resize(nOvrCount);
    3618             :                         }
    3619           0 :                         catch (const std::exception &)
    3620             :                         {
    3621           0 :                             CPLError(CE_Failure, CPLE_OutOfMemory,
    3622             :                                      "Cannot allocate coordinate array");
    3623           0 :                             return CE_Failure;
    3624             :                         }
    3625           4 :                         double dfStart = 0;
    3626           4 :                         double dfIncrement = 0;
    3627           4 :                         if (poSrcVar->IsRegularlySpaced(dfStart, dfIncrement))
    3628             :                         {
    3629             :                             // Recalculate from spacing: overview pixels are
    3630             :                             // centered at (j * factor + (factor-1)/2.0) in
    3631             :                             // source pixel space.
    3632          87 :                             for (size_t j = 0; j < nOvrCount; ++j)
    3633             :                             {
    3634          84 :                                 adfValues[j] =
    3635          84 :                                     dfStart +
    3636          84 :                                     (static_cast<double>(j) * nFactor +
    3637          84 :                                      (nFactor - 1) / 2.0) *
    3638             :                                         dfIncrement;
    3639             :                             }
    3640             :                         }
    3641             :                         else
    3642             :                         {
    3643             :                             // Irregular spacing: subsample by stride.
    3644           1 :                             if (nSrcSize > 100 * 1000 * 1000)
    3645             :                             {
    3646           0 :                                 CPLError(CE_Failure, CPLE_AppDefined,
    3647             :                                          "Source dimension too large "
    3648             :                                          "for coordinate array");
    3649           0 :                                 return CE_Failure;
    3650             :                             }
    3651           1 :                             const size_t nSrcCount =
    3652             :                                 static_cast<size_t>(nSrcSize);
    3653           1 :                             std::vector<double> adfSrc;
    3654             :                             try
    3655             :                             {
    3656           1 :                                 adfSrc.resize(nSrcCount);
    3657             :                             }
    3658           0 :                             catch (const std::exception &)
    3659             :                             {
    3660           0 :                                 CPLError(CE_Failure, CPLE_OutOfMemory,
    3661             :                                          "Cannot allocate source "
    3662             :                                          "coordinate array");
    3663           0 :                                 return CE_Failure;
    3664             :                             }
    3665           1 :                             const GUInt64 anSrcStart[1] = {0};
    3666           1 :                             const size_t anSrcCount[1] = {nSrcCount};
    3667           2 :                             if (!poSrcVar->Read(
    3668             :                                     anSrcStart, anSrcCount, nullptr, nullptr,
    3669           2 :                                     GDALExtendedDataType::Create(GDT_Float64),
    3670           1 :                                     adfSrc.data()))
    3671             :                             {
    3672           0 :                                 CPLError(CE_Failure, CPLE_AppDefined,
    3673             :                                          "Failed to read coordinate "
    3674             :                                          "variable '%s'",
    3675           0 :                                          poSrcVar->GetName().c_str());
    3676           0 :                                 return CE_Failure;
    3677             :                             }
    3678          33 :                             for (size_t j = 0; j < nOvrCount; ++j)
    3679             :                             {
    3680             :                                 // Pick the source index closest to the
    3681             :                                 // overview pixel center.
    3682             :                                 const size_t nSrcIdx = std::min(
    3683          64 :                                     static_cast<size_t>(static_cast<double>(j) *
    3684          32 :                                                             nFactor +
    3685          32 :                                                         nFactor / 2),
    3686          32 :                                     nSrcCount - 1);
    3687          32 :                                 adfValues[j] = adfSrc[nSrcIdx];
    3688             :                             }
    3689             :                         }
    3690           4 :                         const GUInt64 anStart[1] = {0};
    3691           4 :                         const size_t anCount[1] = {nOvrCount};
    3692           8 :                         if (!poCoordArray->Write(
    3693             :                                 anStart, anCount, nullptr, nullptr,
    3694           8 :                                 GDALExtendedDataType::Create(GDT_Float64),
    3695           4 :                                 adfValues.data()))
    3696             :                         {
    3697           0 :                             CPLError(CE_Failure, CPLE_AppDefined,
    3698             :                                      "Failed to write coordinate "
    3699             :                                      "variable for overview");
    3700           0 :                             return CE_Failure;
    3701             :                         }
    3702           8 :                         aoOvrDims.back()->SetIndexingVariable(
    3703           4 :                             std::move(poCoordArray));
    3704             :                     }
    3705             :                 }
    3706             :             }
    3707             :         }
    3708          11 :         aosCreateOptions.SetNameValue("BLOCKSIZE", osBlockSize.c_str());
    3709             : 
    3710             :         // Validate SHARD_CHUNK_SHAPE: inner chunks must divide
    3711             :         // the (capped) block size evenly. Drop sharding if not.
    3712             :         const char *pszShardShape =
    3713          11 :             aosCreateOptions.FetchNameValue("SHARD_CHUNK_SHAPE");
    3714          11 :         if (pszShardShape)
    3715             :         {
    3716             :             const CPLStringList aosShard(
    3717           2 :                 CSLTokenizeString2(pszShardShape, ",", 0));
    3718             :             const CPLStringList aosBlock(
    3719           2 :                 CSLTokenizeString2(osBlockSize.c_str(), ",", 0));
    3720           1 :             bool bShardValid = (aosShard.size() == aosBlock.size());
    3721           3 :             for (int iDim = 0; bShardValid && iDim < aosShard.size(); ++iDim)
    3722             :             {
    3723           2 :                 const auto nInner = static_cast<GUInt64>(atoll(aosShard[iDim]));
    3724           2 :                 const auto nOuter = static_cast<GUInt64>(atoll(aosBlock[iDim]));
    3725           2 :                 if (nInner == 0 || nOuter < nInner || nOuter % nInner != 0)
    3726           0 :                     bShardValid = false;
    3727             :             }
    3728           1 :             if (!bShardValid)
    3729           0 :                 aosCreateOptions.SetNameValue("SHARD_CHUNK_SHAPE", nullptr);
    3730             :         }
    3731             : 
    3732          11 :         auto poOvrArray = poOvrGroup->CreateMDArray(
    3733          11 :             osArrayName, aoOvrDims, GetDataType(), aosCreateOptions.List());
    3734          11 :         if (!poOvrArray)
    3735             :         {
    3736           0 :             CPLError(CE_Failure, CPLE_AppDefined,
    3737             :                      "Cannot create overview array for factor %d", nFactor);
    3738           0 :             return CE_Failure;
    3739             :         }
    3740             : 
    3741          11 :         if (pRawNoData)
    3742           3 :             poOvrArray->SetRawNoDataValue(pRawNoData);
    3743             : 
    3744             :         // Wrap as classic datasets for GDALRegenerateOverviews.
    3745             :         // Non-spatial dims become bands automatically.
    3746             :         std::unique_ptr<GDALDataset> poPrevDS(
    3747          11 :             poChainSource->AsClassicDataset(iXDim, iYDim));
    3748             :         std::unique_ptr<GDALDataset> poOvrDS(
    3749          11 :             poOvrArray->AsClassicDataset(iXDim, iYDim));
    3750          11 :         if (!poPrevDS || !poOvrDS)
    3751             :         {
    3752           0 :             CPLError(CE_Failure, CPLE_AppDefined,
    3753             :                      "Cannot create classic dataset wrapper for resampling");
    3754           0 :             return CE_Failure;
    3755             :         }
    3756             : 
    3757             :         // Resample all bands from previous level into this overview.
    3758          11 :         const int nBands = poPrevDS->GetRasterCount();
    3759             :         const double dfLevelPixels =
    3760          11 :             static_cast<double>(poOvrDS->GetRasterXSize()) *
    3761          11 :             poOvrDS->GetRasterYSize();
    3762          22 :         void *pLevelData = GDALCreateScaledProgress(
    3763             :             dfPixelsProcessed / dfTotalPixels,
    3764          11 :             (dfPixelsProcessed + dfLevelPixels) / dfTotalPixels, pfnProgress,
    3765             :             pProgressData);
    3766             : 
    3767          11 :         CPLErr eErr = CE_None;
    3768          24 :         for (int iBand = 1; iBand <= nBands && eErr == CE_None; ++iBand)
    3769             :         {
    3770          13 :             const double dfBandBase = static_cast<double>(iBand - 1) / nBands;
    3771          13 :             const double dfBandEnd = static_cast<double>(iBand) / nBands;
    3772          13 :             void *pBandData = GDALCreateScaledProgress(
    3773             :                 dfBandBase, dfBandEnd, GDALScaledProgress, pLevelData);
    3774             : 
    3775             :             GDALRasterBandH hOvrBand =
    3776          13 :                 GDALRasterBand::ToHandle(poOvrDS->GetRasterBand(iBand));
    3777          13 :             eErr = GDALRegenerateOverviews(
    3778             :                 GDALRasterBand::ToHandle(poPrevDS->GetRasterBand(iBand)), 1,
    3779             :                 &hOvrBand, pszResampling, GDALScaledProgress, pBandData);
    3780             : 
    3781          13 :             GDALDestroyScaledProgress(pBandData);
    3782             :         }
    3783             : 
    3784          11 :         GDALDestroyScaledProgress(pLevelData);
    3785          11 :         dfPixelsProcessed += dfLevelPixels;
    3786             : 
    3787          11 :         if (eErr != CE_None)
    3788             :         {
    3789           0 :             CPLError(CE_Failure, CPLE_AppDefined,
    3790             :                      "GDALRegenerateOverviews failed for factor %d", nFactor);
    3791           0 :             return CE_Failure;
    3792             :         }
    3793             : 
    3794          11 :         poChainSource = std::move(poOvrArray);
    3795             :     }
    3796             : 
    3797             :     // Write multiscales metadata on parent group.
    3798           9 :     poParentGroup->GenerateMultiscalesMetadata(pszResampling);
    3799             : 
    3800             :     // Reset overview cache so GetOverviewCount() rediscovers.
    3801           9 :     m_bOverviewsLoaded = false;
    3802           9 :     m_apoOverviews.clear();
    3803             : 
    3804           9 :     return CE_None;
    3805             : }

Generated by: LCOV version 1.14