From 2af13e30e7a0d1de226f9326c4f1ce539dd3b783 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 24 Apr 2024 23:44:17 +0200 Subject: [PATCH] HDF5: improve performance of band IRasterIO() on hyperspectral products Currently running gdalcompare.py on a hyperspectral HDF5 product with bands=424, height=501, width=600 and band_chunk_size=27, height_chunk_size=32, width_chunk_size=38 takes more than one hour. With this optimization, this takes 15 seconds. --- .../data/hdf5/dummy_HDFEOS_swath_chunked.h5 | Bin 0 -> 128709 bytes autotest/gdrivers/hdf5.py | 182 +++++++++ frmts/hdf5/hdf5imagedataset.cpp | 371 +++++++++++++++--- 3 files changed, 507 insertions(+), 46 deletions(-) create mode 100644 autotest/gdrivers/data/hdf5/dummy_HDFEOS_swath_chunked.h5 diff --git a/autotest/gdrivers/data/hdf5/dummy_HDFEOS_swath_chunked.h5 b/autotest/gdrivers/data/hdf5/dummy_HDFEOS_swath_chunked.h5 new file mode 100644 index 0000000000000000000000000000000000000000..4ef0faa00f25bf23e74286a668c227a5bac0e977 GIT binary patch literal 128709 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 literal 0 HcmV?d00001 diff --git a/autotest/gdrivers/hdf5.py b/autotest/gdrivers/hdf5.py index ce4cd94787ee..c03580092d5c 100755 --- a/autotest/gdrivers/hdf5.py +++ b/autotest/gdrivers/hdf5.py @@ -1231,3 +1231,185 @@ def test_hdf5_band_specific_attribute(): "bad_band": "1", } ds = None + + +############################################################################### +# Test opening a chunked HDF5EOS swath file + + +def test_hdf5_eos_swath_chunking_optimization(): + + if False: + + import h5py + import numpy as np + + f = h5py.File("data/hdf5/dummy_HDFEOS_swath_chunked.h5", "w") + HDFEOS_INFORMATION = f.create_group("HDFEOS INFORMATION") + # Hint from https://forum.hdfgroup.org/t/nullpad-nullterm-strings/9107 + # to use the low-level API to be able to generate NULLTERM strings + # without padding bytes + HDFEOSVersion_type = h5py.h5t.TypeID.copy(h5py.h5t.C_S1) + HDFEOSVersion_type.set_size(32) + HDFEOSVersion_type.set_strpad(h5py.h5t.STR_NULLTERM) + # HDFEOS_INFORMATION.attrs.create("HDFEOSVersion", "HDFEOS_5.1.15", dtype=HDFEOSVersion_type) + HDFEOSVersion_attr = h5py.h5a.create( + HDFEOS_INFORMATION.id, + "HDFEOSVersion".encode("ASCII"), + HDFEOSVersion_type, + h5py.h5s.create(h5py.h5s.SCALAR), + ) + HDFEOSVersion_value = "HDFEOS_5.1.15".encode("ASCII") + HDFEOSVersion_value = np.frombuffer( + HDFEOSVersion_value, dtype="|S%d" % len(HDFEOSVersion_value) + ) + HDFEOSVersion_attr.write(HDFEOSVersion_value) + + StructMetadata_0_type = h5py.h5t.TypeID.copy(h5py.h5t.C_S1) + StructMetadata_0_type.set_size(32000) + StructMetadata_0_type.set_strpad(h5py.h5t.STR_NULLTERM) + StructMetadata_0 = """GROUP=SwathStructure + GROUP=SWATH_1 + SwathName="MySwath" + GROUP=Dimension + OBJECT=Dimension_1 + DimensionName="Band" + Size=20 + END_OBJECT=Dimension_1 + OBJECT=Dimension_2 + DimensionName="AlongTrack" + Size=30 + END_OBJECT=Dimension_2 + OBJECT=Dimension_3 + DimensionName="CrossTrack" + Size=40 + END_OBJECT=Dimension_3 + END_GROUP=Dimension + GROUP=DimensionMap + END_GROUP=DimensionMap + GROUP=IndexDimensionMap + END_GROUP=IndexDimensionMap + GROUP=GeoField + OBJECT=GeoField_1 + GeoFieldName="Latitude" + DataType=H5T_NATIVE_FLOAT + DimList=("AlongTrack","CrossTrack") + MaxdimList=("AlongTrack","CrossTrack") + END_OBJECT=GeoField_1 + OBJECT=GeoField_2 + GeoFieldName="Longitude" + DataType=H5T_NATIVE_FLOAT + DimList=("AlongTrack","CrossTrack") + MaxdimList=("AlongTrack","CrossTrack") + END_OBJECT=GeoField_2 + OBJECT=GeoField_3 + GeoFieldName="Time" + DataType=H5T_NATIVE_FLOAT + DimList=("AlongTrack") + MaxdimList=("AlongTrack") + END_OBJECT=GeoField_3 + END_GROUP=GeoField + GROUP=DataField + OBJECT=DataField_1 + DataFieldName="MyDataField" + DataType=H5T_NATIVE_FLOAT + DimList=("Band","AlongTrack","CrossTrack") + MaxdimList=("Band","AlongTrack","CrossTrack") + END_OBJECT=DataField_1 + END_GROUP=DataField + GROUP=ProfileField + END_GROUP=ProfileField + GROUP=MergedFields + END_GROUP=MergedFields + END_GROUP=SWATH_1 +END_GROUP=SwathStructure +GROUP=GridStructure +END_GROUP=GridStructure +END +""" + StructMetadata_0_dataset = h5py.h5d.create( + HDFEOS_INFORMATION.id, + "StructMetadata.0".encode("ASCII"), + StructMetadata_0_type, + h5py.h5s.create(h5py.h5s.SCALAR), + ) + StructMetadata_0_value = StructMetadata_0.encode("ASCII") + StructMetadata_0_value = np.frombuffer( + StructMetadata_0_value, dtype="|S%d" % len(StructMetadata_0_value) + ) + StructMetadata_0_dataset.write( + h5py.h5s.create(h5py.h5s.SCALAR), + h5py.h5s.create(h5py.h5s.SCALAR), + StructMetadata_0_value, + ) + + HDFEOS = f.create_group("HDFEOS") + ADDITIONAL = HDFEOS.create_group("ADDITIONAL") + ADDITIONAL.create_group("FILE_ATTRIBUTES") + SWATHS = HDFEOS.create_group("SWATHS") + MySwath = SWATHS.create_group("MySwath") + DataFields = MySwath.create_group("Data Fields") + ds = DataFields.create_dataset( + "MyDataField", (20, 30, 40), chunks=(3, 4, 6), dtype="f", compression="gzip" + ) + ds[...] = np.array([i for i in range(20 * 30 * 40)]).reshape(ds.shape) + GeoLocationFields = MySwath.create_group("Geolocation Fields") + ds = GeoLocationFields.create_dataset("Longitude", (20, 30), dtype="f") + ds[...] = np.array([i for i in range(20 * 30)]).reshape(ds.shape) + ds = GeoLocationFields.create_dataset("Latitude", (20, 30), dtype="f") + ds[...] = np.array([i for i in range(20 * 30)]).reshape(ds.shape) + f.close() + + ds = gdal.Open( + 'HDF5:"data/hdf5/dummy_HDFEOS_swath_chunked.h5"://HDFEOS/SWATHS/MySwath/Data_Fields/MyDataField' + ) + mem_ds = gdal.Translate("", ds, format="MEM") + + ds.GetRasterBand(1).ReadRaster(0, 0, 1, 1) + assert ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") == "DISABLED" + + ds.GetRasterBand(1).ReadRaster(0, 0, ds.RasterXSize, 1) + assert ( + ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") + == "DETECTION_IN_PROGRESS" + ) + ds.GetRasterBand(1).ReadRaster(0, 2, ds.RasterXSize, 1) + assert ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") == "DISABLED" + + ds.GetRasterBand(1).ReadRaster(0, 0, ds.RasterXSize, 1) + assert ( + ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") + == "DETECTION_IN_PROGRESS" + ) + ds.GetRasterBand(2).ReadRaster(0, 0, ds.RasterXSize, 1) + assert ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") == "DISABLED" + + ds = gdal.Open( + 'HDF5:"data/hdf5/dummy_HDFEOS_swath_chunked.h5"://HDFEOS/SWATHS/MySwath/Data_Fields/MyDataField' + ) + assert ( + ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") + == "DETECTION_IN_PROGRESS" + ) + for i in range(ds.RasterCount): + assert ( + ds.GetRasterBand(i + 1).Checksum() == mem_ds.GetRasterBand(i + 1).Checksum() + ) + assert ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") == ( + "DETECTION_IN_PROGRESS" if i == 0 else "ENABLED" + ) + for i in range(ds.RasterCount): + assert ( + ds.GetRasterBand(i + 1).ReadRaster() + == mem_ds.GetRasterBand(i + 1).ReadRaster() + ) + assert ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") == "ENABLED" + assert ds.GetRasterBand(i + 1).ReadRaster(1, 2, 3, 4) == mem_ds.GetRasterBand( + i + 1 + ).ReadRaster(1, 2, 3, 4) + assert ds.GetMetadataItem("WholeBandChunkOptim", "__DEBUG__") == "ENABLED" + + blockxsize, blockysize = ds.GetRasterBand(1).GetBlockSize() + assert ds.GetRasterBand(1).ReadBlock(1, 2) == mem_ds.GetRasterBand(1).ReadRaster( + 1 * blockxsize, 2 * blockysize, blockxsize, blockysize + ) diff --git a/frmts/hdf5/hdf5imagedataset.cpp b/frmts/hdf5/hdf5imagedataset.cpp index 9bfb2e22914b..07e31f98282e 100644 --- a/frmts/hdf5/hdf5imagedataset.cpp +++ b/frmts/hdf5/hdf5imagedataset.cpp @@ -83,6 +83,37 @@ class HDF5ImageDataset final : public HDF5Dataset int m_nYIndex = -1; int m_nOtherDimIndex = -1; + int m_nBlockXSize = 0; + int m_nBlockYSize = 0; + int m_nBandChunkSize = 1; //! Number of bands in a chunk + + enum WholeBandChunkOptim + { + WBC_DETECTION_IN_PROGRESS, + WBC_DISABLED, + WBC_ENABLED, + }; + + //! Flag to detect if the read pattern of HDF5ImageRasterBand::IRasterIO() + // is whole band after whole band. + WholeBandChunkOptim m_eWholeBandChunkOptim = WBC_DETECTION_IN_PROGRESS; + //! Value of nBand during last HDF5ImageRasterBand::IRasterIO() call + int m_nLastRasterIOBand = -1; + //! Value of nXOff during last HDF5ImageRasterBand::IRasterIO() call + int m_nLastRasterIOXOff = -1; + //! Value of nYOff during last HDF5ImageRasterBand::IRasterIO() call + int m_nLastRasterIOYOff = -1; + //! Value of nXSize during last HDF5ImageRasterBand::IRasterIO() call + int m_nLastRasterIOXSize = -1; + //! Value of nYSize during last HDF5ImageRasterBand::IRasterIO() call + int m_nLastRasterIOYSize = -1; + //! Value such that m_abyBandChunk represent band data in the range + // [m_iCurrentBandChunk * m_nBandChunkSize, (m_iCurrentBandChunk+1) * m_nBandChunkSize[ + int m_iCurrentBandChunk = -1; + //! Cached values (in native data type) for bands in the range + // [m_iCurrentBandChunk * m_nBandChunkSize, (m_iCurrentBandChunk+1) * m_nBandChunkSize[ + std::vector m_abyBandChunk{}; + CPLErr CreateODIMH5Projection(); public: @@ -106,6 +137,9 @@ class HDF5ImageDataset final : public HDF5Dataset GSpacing nBandSpace, GDALRasterIOExtraArg *psExtraArg) override; + const char *GetMetadataItem(const char *pszName, + const char *pszDomain = "") override; + Hdf5ProductType GetSubdatasetType() const { return iSubdatasetType; @@ -228,8 +262,9 @@ class HDF5ImageRasterBand final : public GDALPamRasterBand { friend class HDF5ImageDataset; - bool bNoDataSet; - double dfNoDataValue; + bool bNoDataSet = false; + double dfNoDataValue = -9999.0; + int m_nIRasterIORecCounter = 0; public: HDF5ImageRasterBand(HDF5ImageDataset *, int, GDALDataType); @@ -259,32 +294,12 @@ HDF5ImageRasterBand::~HDF5ImageRasterBand() /************************************************************************/ HDF5ImageRasterBand::HDF5ImageRasterBand(HDF5ImageDataset *poDSIn, int nBandIn, GDALDataType eType) - : bNoDataSet(false), dfNoDataValue(-9999.0) { poDS = poDSIn; nBand = nBandIn; eDataType = eType; - nBlockXSize = poDS->GetRasterXSize(); - nBlockYSize = 1; - - // Check for chunksize and set it as the blocksize (optimizes read). - const hid_t listid = H5Dget_create_plist(poDSIn->dataset_id); - if (listid > 0) - { - if (H5Pget_layout(listid) == H5D_CHUNKED) - { - hsize_t panChunkDims[3] = {0, 0, 0}; - const int nDimSize = H5Pget_chunk(listid, 3, panChunkDims); - CPL_IGNORE_RET_VAL(nDimSize); - CPLAssert(nDimSize == poDSIn->ndims); - nBlockXSize = static_cast(panChunkDims[poDSIn->GetXIndex()]); - if (poDSIn->GetYIndex() >= 0) - nBlockYSize = - static_cast(panChunkDims[poDSIn->GetYIndex()]); - } - - H5Pclose(listid); - } + nBlockXSize = poDSIn->m_nBlockXSize; + nBlockYSize = poDSIn->m_nBlockYSize; // netCDF convention for nodata bNoDataSet = @@ -316,8 +331,6 @@ double HDF5ImageRasterBand::GetNoDataValue(int *pbSuccess) CPLErr HDF5ImageRasterBand::IReadBlock(int nBlockXOff, int nBlockYOff, void *pImage) { - HDF5_GLOBAL_LOCK(); - HDF5ImageDataset *poGDS = static_cast(poDS); memset(pImage, 0, @@ -329,6 +342,29 @@ CPLErr HDF5ImageRasterBand::IReadBlock(int nBlockXOff, int nBlockYOff, return CE_None; } + const int nXOff = nBlockXOff * nBlockXSize; + const int nYOff = nBlockYOff * nBlockYSize; + const int nXSize = std::min(nBlockXSize, nRasterXSize - nXOff); + const int nYSize = std::min(nBlockYSize, nRasterYSize - nYOff); + if (poGDS->m_eWholeBandChunkOptim == HDF5ImageDataset::WBC_ENABLED) + { + const bool bIsBandInterleavedData = + poGDS->ndims == 3 && poGDS->m_nOtherDimIndex == 0 && + poGDS->GetYIndex() == 1 && poGDS->GetXIndex() == 2; + if (poGDS->nBands == 1 || bIsBandInterleavedData) + { + GDALRasterIOExtraArg sExtraArg; + INIT_RASTERIO_EXTRA_ARG(sExtraArg); + const int nDTSize = GDALGetDataTypeSizeBytes(eDataType); + return IRasterIO(GF_Read, nXOff, nYOff, nXSize, nYSize, pImage, + nXSize, nYSize, eDataType, nDTSize, + static_cast(nDTSize) * nBlockXSize, + &sExtraArg); + } + } + + HDF5_GLOBAL_LOCK(); + hsize_t count[3] = {0, 0, 0}; H5OFFSET_TYPE offset[3] = {0, 0, 0}; hsize_t col_dims[3] = {0, 0, 0}; @@ -343,22 +379,14 @@ CPLErr HDF5ImageRasterBand::IReadBlock(int nBlockXOff, int nBlockYOff, } const int nYIndex = poGDS->GetYIndex(); - if (nYIndex >= 0) - offset[nYIndex] = nBlockYOff * static_cast(nBlockYSize); - offset[poGDS->GetXIndex()] = nBlockXOff * static_cast(nBlockXSize); - if (nYIndex >= 0) - count[nYIndex] = nBlockYSize; - count[poGDS->GetXIndex()] = nBlockXSize; - // Blocksize may not be a multiple of imagesize. if (nYIndex >= 0) { - count[nYIndex] = std::min(hsize_t(nBlockYSize), - poDS->GetRasterYSize() - offset[nYIndex]); + offset[nYIndex] = nYOff; + count[nYIndex] = nYSize; } - count[poGDS->GetXIndex()] = - std::min(hsize_t(nBlockXSize), - poDS->GetRasterXSize() - offset[poGDS->GetXIndex()]); + offset[poGDS->GetXIndex()] = nXOff; + count[poGDS->GetXIndex()] = nXSize; // Select block from file space. herr_t status = H5Sselect_hyperslab(poGDS->dataspace_id, H5S_SELECT_SET, @@ -410,14 +438,187 @@ CPLErr HDF5ImageRasterBand::IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, { HDF5ImageDataset *poGDS = static_cast(poDS); - const bool bIsExpectedLayout = - ((poGDS->ndims == 3 && poGDS->m_nOtherDimIndex == 0 && - poGDS->GetYIndex() == 1 && poGDS->GetXIndex() == 2) || - (poGDS->ndims == 2 && poGDS->GetYIndex() == 0 && - poGDS->GetXIndex() == 1)); + const bool bIsBandInterleavedData = + poGDS->ndims == 3 && poGDS->m_nOtherDimIndex == 0 && + poGDS->GetYIndex() == 1 && poGDS->GetXIndex() == 2; const int nDTSize = GDALGetDataTypeSizeBytes(eDataType); + // Try to detect if we read whole bands by chunks of whole lines + // If so, then read and cache whole band (or group of m_nBandChunkSize bands) + // to save HDF5 decompression. + if (m_nIRasterIORecCounter == 0) + { + bool bInvalidateWholeBandChunkOptim = false; + if (!(nXSize == nBufXSize && nYSize == nBufYSize)) + { + bInvalidateWholeBandChunkOptim = true; + } + // Is the first request on band 1, line 0 and one or several full lines? + else if (poGDS->m_eWholeBandChunkOptim != + HDF5ImageDataset::WBC_ENABLED && + nBand == 1 && nXOff == 0 && nYOff == 0 && + nXSize == nRasterXSize) + { + poGDS->m_eWholeBandChunkOptim = + HDF5ImageDataset::WBC_DETECTION_IN_PROGRESS; + poGDS->m_nLastRasterIOBand = 1; + poGDS->m_nLastRasterIOXOff = nXOff; + poGDS->m_nLastRasterIOYOff = nYOff; + poGDS->m_nLastRasterIOXSize = nXSize; + poGDS->m_nLastRasterIOYSize = nYSize; + } + else if (poGDS->m_eWholeBandChunkOptim == + HDF5ImageDataset::WBC_DETECTION_IN_PROGRESS) + { + if (poGDS->m_nLastRasterIOBand == 1 && nBand == 1) + { + // Is this request a continuation of the previous one? + if (nXOff == 0 && poGDS->m_nLastRasterIOXOff == 0 && + nYOff == poGDS->m_nLastRasterIOYOff + + poGDS->m_nLastRasterIOYSize && + poGDS->m_nLastRasterIOXSize == nRasterXSize && + nXSize == nRasterXSize) + { + poGDS->m_nLastRasterIOXOff = nXOff; + poGDS->m_nLastRasterIOYOff = nYOff; + poGDS->m_nLastRasterIOXSize = nXSize; + poGDS->m_nLastRasterIOYSize = nYSize; + } + else + { + bInvalidateWholeBandChunkOptim = true; + } + } + else if (poGDS->m_nLastRasterIOBand == 1 && nBand == 2) + { + // Are we switching to band 2 while having fully read band 1? + if (nXOff == 0 && nYOff == 0 && nXSize == nRasterXSize && + poGDS->m_nLastRasterIOXOff == 0 && + poGDS->m_nLastRasterIOXSize == nRasterXSize && + poGDS->m_nLastRasterIOYOff + poGDS->m_nLastRasterIOYSize == + nRasterYSize) + { + if ((poGDS->m_nBandChunkSize > 1 || + nBufYSize < nRasterYSize) && + static_cast(poGDS->m_nBandChunkSize) * + nRasterXSize * nRasterYSize * nDTSize < + CPLGetUsablePhysicalRAM() / 10) + { + poGDS->m_eWholeBandChunkOptim = + HDF5ImageDataset::WBC_ENABLED; + } + else + { + bInvalidateWholeBandChunkOptim = true; + } + } + else + { + bInvalidateWholeBandChunkOptim = true; + } + } + else + { + bInvalidateWholeBandChunkOptim = true; + } + } + if (bInvalidateWholeBandChunkOptim) + { + poGDS->m_eWholeBandChunkOptim = HDF5ImageDataset::WBC_DISABLED; + poGDS->m_nLastRasterIOBand = -1; + poGDS->m_nLastRasterIOXOff = -1; + poGDS->m_nLastRasterIOYOff = -1; + poGDS->m_nLastRasterIOXSize = -1; + poGDS->m_nLastRasterIOYSize = -1; + } + } + + if (poGDS->m_eWholeBandChunkOptim == HDF5ImageDataset::WBC_ENABLED && + nXSize == nBufXSize && nYSize == nBufYSize) + { + if (poGDS->nBands == 1 || bIsBandInterleavedData) + { + if (poGDS->m_iCurrentBandChunk < 0) + CPLDebug("HDF5", "Using whole band chunk caching"); + const int iBandChunk = (nBand - 1) / poGDS->m_nBandChunkSize; + if (iBandChunk != poGDS->m_iCurrentBandChunk) + { + poGDS->m_abyBandChunk.resize( + static_cast(poGDS->m_nBandChunkSize) * + nRasterXSize * nRasterYSize * nDTSize); + + HDF5_GLOBAL_LOCK(); + + hsize_t count[3] = { + static_cast( + std::min(poGDS->nBands, + (iBandChunk + 1) * poGDS->m_nBandChunkSize) - + iBandChunk * poGDS->m_nBandChunkSize), + static_cast(nRasterYSize), + static_cast(nRasterXSize)}; + H5OFFSET_TYPE offset[3] = { + static_cast(iBandChunk * + poGDS->m_nBandChunkSize), + static_cast(0), + static_cast(0)}; + herr_t status = + H5Sselect_hyperslab(poGDS->dataspace_id, H5S_SELECT_SET, + offset, nullptr, count, nullptr); + if (status < 0) + return CE_Failure; + + const hid_t memspace = + H5Screate_simple(poGDS->ndims, count, nullptr); + H5OFFSET_TYPE mem_offset[3] = {0, 0, 0}; + status = + H5Sselect_hyperslab(memspace, H5S_SELECT_SET, mem_offset, + nullptr, count, nullptr); + if (status < 0) + { + H5Sclose(memspace); + return CE_Failure; + } + + status = H5Dread(poGDS->dataset_id, poGDS->native, memspace, + poGDS->dataspace_id, H5P_DEFAULT, + poGDS->m_abyBandChunk.data()); + + H5Sclose(memspace); + + if (status < 0) + { + CPLError( + CE_Failure, CPLE_AppDefined, + "HDF5ImageRasterBand::IRasterIO(): H5Dread() failed"); + return CE_Failure; + } + + poGDS->m_iCurrentBandChunk = iBandChunk; + } + + for (int iY = 0; iY < nYSize; ++iY) + { + GDALCopyWords(poGDS->m_abyBandChunk.data() + + static_cast((nBand - 1) % + poGDS->m_nBandChunkSize) * + nRasterYSize * nRasterXSize * nDTSize + + static_cast(nYOff + iY) * + nRasterXSize * nDTSize + + nXOff * nDTSize, + eDataType, nDTSize, + static_cast(pData) + + static_cast(iY) * nLineSpace, + eBufType, static_cast(nPixelSpace), nXSize); + } + return CE_None; + } + } + + const bool bIsExpectedLayout = + (bIsBandInterleavedData || + (poGDS->ndims == 2 && poGDS->GetYIndex() == 0 && + poGDS->GetXIndex() == 1)); if (eRWFlag == GF_Read && bIsExpectedLayout && nXSize == nBufXSize && nYSize == nBufYSize && eBufType == eDataType && nPixelSpace == nDTSize && nLineSpace == nXSize * nPixelSpace) @@ -482,10 +683,13 @@ CPLErr HDF5ImageRasterBand::IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, CPLAssert(pMemData); // Read from HDF5 into the temporary MEMDataset using the // natural interleaving of the HDF5 dataset - if (IRasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pMemData, + ++m_nIRasterIORecCounter; + CPLErr eErr = + IRasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pMemData, nXSize, nYSize, eDataType, nDTSize, - static_cast(nXSize) * nDTSize, - psExtraArg) != CE_None) + static_cast(nXSize) * nDTSize, psExtraArg); + --m_nIRasterIORecCounter; + if (eErr != CE_None) { return CE_Failure; } @@ -950,6 +1154,58 @@ GDALDataset *HDF5ImageDataset::Open(GDALOpenInfo *poOpenInfo) } } + poDS->m_nBlockXSize = poDS->GetRasterXSize(); + poDS->m_nBlockYSize = 1; + poDS->m_nBandChunkSize = 1; + + // Check for chunksize and set it as the blocksize (optimizes read). + const hid_t listid = H5Dget_create_plist(poDS->dataset_id); + if (listid > 0) + { + if (H5Pget_layout(listid) == H5D_CHUNKED) + { + hsize_t panChunkDims[3] = {0, 0, 0}; + const int nDimSize = H5Pget_chunk(listid, 3, panChunkDims); + CPL_IGNORE_RET_VAL(nDimSize); + CPLAssert(nDimSize == poDS->ndims); + poDS->m_nBlockXSize = + static_cast(panChunkDims[poDS->GetXIndex()]); + if (poDS->GetYIndex() >= 0) + poDS->m_nBlockYSize = + static_cast(panChunkDims[poDS->GetYIndex()]); + if (nBands > 1) + { + poDS->m_nBandChunkSize = + static_cast(panChunkDims[poDS->m_nOtherDimIndex]); + + poDS->SetMetadataItem("BAND_CHUNK_SIZE", + CPLSPrintf("%d", poDS->m_nBandChunkSize), + "IMAGE_STRUCTURE"); + } + } + + const int nFilters = H5Pget_nfilters(listid); + for (int i = 0; i < nFilters; ++i) + { + unsigned int flags = 0; + size_t cd_nelmts = 0; + char szName[64 + 1] = {0}; + const auto eFilter = H5Pget_filter(listid, i, &flags, &cd_nelmts, + nullptr, 64, szName); + if (eFilter == H5Z_FILTER_DEFLATE) + { + poDS->SetMetadataItem("COMPRESSION", "DEFLATE", + "IMAGE_STRUCTURE"); + } + else if (eFilter == H5Z_FILTER_SZIP) + { + poDS->SetMetadataItem("COMPRESSION", "SZIP", "IMAGE_STRUCTURE"); + } + } + + H5Pclose(listid); + } + for (int i = 0; i < nBands; i++) { HDF5ImageRasterBand *const poBand = new HDF5ImageRasterBand( @@ -1286,6 +1542,29 @@ CPLErr HDF5ImageDataset::CreateProjections() return CE_None; } +/************************************************************************/ +/* GetMetadataItem() */ +/************************************************************************/ + +const char *HDF5ImageDataset::GetMetadataItem(const char *pszName, + const char *pszDomain) +{ + if (pszDomain && EQUAL(pszDomain, "__DEBUG__") && + EQUAL(pszName, "WholeBandChunkOptim")) + { + switch (m_eWholeBandChunkOptim) + { + case WBC_DETECTION_IN_PROGRESS: + return "DETECTION_IN_PROGRESS"; + case WBC_DISABLED: + return "DISABLED"; + case WBC_ENABLED: + return "ENABLED"; + } + } + return GDALPamDataset::GetMetadataItem(pszName, pszDomain); +} + /************************************************************************/ /* GetSpatialRef() */ /************************************************************************/