@@ -40,6 +40,7 @@ struct pool_pipeline_t : public pipeline_t<pool_pipeline_t> {
4040 // pipeline (and its shared_ptr to the pool) goes away. Blocks the
4141 // destructor on each front future.
4242 while (!in_flight_.empty ()) drain_in_flight (/* blocking=*/ true );
43+ while (!read_in_flight_.empty ()) drain_in_flight_read (nullptr , /* blocking=*/ true );
4344 }
4445
4546 pool_pipeline_t (const pool_pipeline_t &) = delete ;
@@ -52,11 +53,19 @@ struct pool_pipeline_t : public pipeline_t<pool_pipeline_t> {
5253 void write_chunk_impl (const hsize_t * offset, std::size_t nbytes, const void * src);
5354 void read_chunk_impl (const hsize_t * offset, std::size_t nbytes, void * dst);
5455
56+ // Parallel read override — shadows pipeline_t<pool_pipeline_t>::read.
57+ // Rank-1 chunked datasets are decompressed across the worker pool;
58+ // rank>1 falls back to the synchronous base-class path.
59+ void read (const h5::ds_t & ds, const h5::offset_t & offset,
60+ const h5::stride_t & stride, const h5::block_t & block,
61+ const h5::count_t & count, const h5::dxpl_t & dxpl, void * ptr);
62+
5563 // Public quiesce — block until every in-flight future has been
5664 // drained. Called by pt_t::flush so users get the expected
5765 // "data on disk after flush returns" semantics for the pool path.
5866 void drain () {
5967 while (!in_flight_.empty ()) drain_in_flight (/* blocking=*/ true );
68+ while (!read_in_flight_.empty ()) drain_in_flight_read (nullptr , /* blocking=*/ true );
6069 }
6170
6271private:
@@ -72,9 +81,20 @@ struct pool_pipeline_t : public pipeline_t<pool_pipeline_t> {
7281
7382 void drain_in_flight (bool blocking);
7483
84+ // Result of a worker decompression task for the parallel read path.
85+ struct read_result_t {
86+ std::unique_ptr<std::byte[]> data;
87+ std::size_t copy_size{0 };
88+ std::size_t dst_offset{0 };
89+ };
90+
91+ void drain_in_flight_read (char * dst, bool blocking);
92+
7593 std::shared_ptr<worker_pool_t > pool_;
7694 unsigned cap_{0 };
7795 std::deque<std::future<result_t >> in_flight_;
96+ std::deque<std::future<read_result_t >> read_in_flight_;
97+ char * read_dst_ = nullptr ;
7898};
7999
80100// ─── write path ──────────────────────────────────────────────────────────────
@@ -202,13 +222,144 @@ inline void pool_pipeline_t::write_chunk_impl(const hsize_t* offset_in,
202222 drain_in_flight (/* blocking=*/ true );
203223}
204224
205- // ─── read path (synchronous for v1) ──────────────────────────────────────────
225+ // ─── read path ───────────────────── ──────────────────────────────────────────
206226//
207- // Phase 1.3.3 keeps read synchronous, identical to basic_pipeline_t::read_chunk_impl.
208- // Parallel decompression is a deliberate follow-up: it requires read-ahead
209- // (the read path consumes chunks in order, but the user's buffer slots are
210- // known up-front, so prefetching can fan out across the pool). Tracked in
211- // Phase 1.5+ work.
227+ // Phase 1.3.3 kept read synchronous. Phase 1.5 adds parallel decompression
228+ // for rank-1 chunked datasets by running the reverse filter chain on the
229+ // worker pool while the calling thread performs sequential H5Dread_chunk I/O.
230+
231+ inline void pool_pipeline_t::drain_in_flight_read (char * dst, bool blocking) {
232+ using namespace std ::chrono_literals;
233+ char * base = dst ? dst : read_dst_;
234+ if (!base) return ; // no destination available (shouldn't happen outside read())
235+
236+ if (blocking) {
237+ if (read_in_flight_.empty ()) return ;
238+ auto r = read_in_flight_.front ().get ();
239+ std::memcpy (base + r.dst_offset , r.data .get (), r.copy_size );
240+ read_in_flight_.pop_front ();
241+ return ;
242+ }
243+ while (!read_in_flight_.empty () &&
244+ read_in_flight_.front ().wait_for (0s) == std::future_status::ready) {
245+ auto r = read_in_flight_.front ().get ();
246+ std::memcpy (base + r.dst_offset , r.data .get (), r.copy_size );
247+ read_in_flight_.pop_front ();
248+ }
249+ }
250+
251+ inline void pool_pipeline_t::read (const h5::ds_t & ds, const h5::offset_t & offset,
252+ const h5::stride_t & stride, const h5::block_t & block,
253+ const h5::count_t & count, const h5::dxpl_t & dxpl, void * ptr) {
254+ // Fallback to synchronous base-class path when no pool is present or when
255+ // the dataset is not rank-1. Rank>1 parallel decompression is future work.
256+ if (!pool_ || rank != 1 ) {
257+ pipeline_t <pool_pipeline_t >::read (ds, offset, stride, block, count, dxpl, ptr);
258+ return ;
259+ }
260+
261+ this ->dxpl = dxpl;
262+ this ->ds = ds;
263+
264+ h5::offset_t offset_;
265+ h5::count_t count_;
266+ for (hsize_t i = 0 ; i < rank; ++i) {
267+ offset_[i] = offset[i];
268+ count_[i] = count[i] * block[i];
269+ }
270+
271+ // Rank-1 chunk decomposition (mirrors split_to_chunk_read).
272+ hsize_t n = count_[0 ]; // total elements to read
273+ hsize_t b = B[0 ]; // elements per chunk
274+ hsize_t ry = n % b; // trailing partial chunk
275+
276+ // Snapshot filter chain into a POD closure capture (same pattern as write).
277+ struct filter_chain_t {
278+ filter::call_t filter[H5CPP_MAX_FILTER ];
279+ unsigned flags[H5CPP_MAX_FILTER ];
280+ std::size_t cd_size[H5CPP_MAX_FILTER ];
281+ unsigned cd_values[H5CPP_MAX_FILTER ][H5CPP_MAX_FILTER_PARAM ];
282+ hsize_t tail;
283+ };
284+ filter_chain_t fc{};
285+ fc.tail = this ->tail ;
286+ std::memcpy (fc.filter , this ->filter , sizeof (fc.filter ));
287+ std::memcpy (fc.flags , this ->flags , sizeof (fc.flags ));
288+ std::memcpy (fc.cd_size , this ->cd_size , sizeof (fc.cd_size ));
289+ std::memcpy (fc.cd_values , this ->cd_values , sizeof (fc.cd_values ));
290+
291+ const std::size_t scratch = filter::filter_scratch_bound (block_size);
292+ char * dst = static_cast <char *>(ptr);
293+ read_dst_ = dst;
294+ const std::size_t block_sz = this ->block_size ;
295+
296+ for (hsize_t j = 0 ; j < n; j += b) {
297+ hsize_t chunk_offset = j + offset_[0 ];
298+ hsize_t copy_size = (ry != 0 && j == n - ry) ? ry * element_size : b * element_size;
299+ hsize_t dst_offset = j * element_size;
300+
301+ // Main thread performs HDF5 I/O (H5Dread_chunk is not thread-safe).
302+ auto compressed = std::make_unique<std::byte[]>(scratch);
303+ std::uint32_t filter_mask = 0 ;
304+ hsize_t C[1 ] = {chunk_offset};
305+ #if H5_VERSION_GE(2,0,0)
306+ std::size_t buf_size = scratch;
307+ H5Dread_chunk2 (static_cast <::hid_t >(ds), static_cast <::hid_t >(dxpl),
308+ C, &filter_mask, compressed.get (), &buf_size);
309+ #else
310+ H5Dread_chunk (static_cast <::hid_t >(ds), static_cast <::hid_t >(dxpl),
311+ C, &filter_mask, compressed.get ());
312+ #endif
313+
314+ // Submit decompression to the worker pool.
315+ auto fut = pool_->submit ([
316+ compressed = std::move (compressed), block_sz, copy_size,
317+ dst_offset, scratch, fc
318+ ]() mutable -> read_result_t {
319+ read_result_t result;
320+ result.copy_size = copy_size;
321+ result.dst_offset = dst_offset;
322+
323+ if (fc.tail == 0 ) {
324+ result.data = std::make_unique<std::byte[]>(copy_size);
325+ std::memcpy (result.data .get (), compressed.get (), copy_size);
326+ return result;
327+ }
328+
329+ auto work_buf = std::make_unique<std::byte[]>(scratch);
330+ void * src = compressed.get ();
331+ void * dst_buf = work_buf.get ();
332+ std::size_t length = block_sz;
333+
334+ for (hsize_t fi = fc.tail ; fi > 0 ; --fi) {
335+ const hsize_t idx = fi - 1 ;
336+ length = fc.filter [idx](dst_buf, src, length,
337+ fc.flags [idx] | H5Z_FLAG_REVERSE ,
338+ fc.cd_size [idx], fc.cd_values [idx]);
339+ std::swap (src, dst_buf);
340+ }
341+
342+ result.data = std::make_unique<std::byte[]>(copy_size);
343+ std::memcpy (result.data .get (), src, copy_size);
344+ return result;
345+ });
346+
347+ read_in_flight_.push_back (std::move (fut));
348+
349+ // Opportunistic drain — if front future is ready, copy and free buffer.
350+ drain_in_flight_read (dst, /* blocking=*/ false );
351+
352+ // Bounded back-pressure: block when the deque reaches the cap.
353+ while (read_in_flight_.size () >= cap_)
354+ drain_in_flight_read (dst, /* blocking=*/ true );
355+ }
356+
357+ // Drain any remaining decompression tasks.
358+ while (!read_in_flight_.empty ())
359+ drain_in_flight_read (dst, /* blocking=*/ true );
360+
361+ read_dst_ = nullptr ;
362+ }
212363
213364inline void pool_pipeline_t::read_chunk_impl (const hsize_t * offset_in,
214365 std::size_t nbytes,
0 commit comments