From c985438db10ac503d9f270f605903d409ab53b09 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 6 Aug 2026 01:08:50 +0200 Subject: [PATCH] test(pf-bitstream): replay real host captures through the planners + HEVC goldens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M0's capture hook has been in since 119ec0dd with nothing consuming its output. corpus_replay.rs is that consumer: point PF_CORPUS at an au-. capture and every AU walks back through the H.264 or H.265 planner, asserting no errors and no warnings — a clean capture of a healthy session must plan whole. Ignored by default (captures are hundreds of MB and live outside the repo). It earns its keep immediately. Captured on .173 against the live host (NVENC, 2800x1260, ~30 s each, client-side codec pin only — no host config touched): h265 1133/1133 AUs planned, 0 errors, 0 warnings h264 1514/1514 AUs planned, 0 errors, 0 warnings The HEVC number is the point: it is the FIRST validation of the WP-1 h265 planner against real host output rather than the vendored conformance vectors, and it lands before the client's HEVC rung exists to produce on-glass evidence. Two real-capture facts the harness had to learn, both from this run: ending a capture means killing the client, so the final .idx line is routinely half-written and the final AU's bytes may not all have landed. Both are tolerated at the TAIL only — a malformed line anywhere else, or a gap the data cannot cover mid-file, still fails loudly rather than silently replaying a subset. Also adds tests/data/test-25fps-h265.nv12.sha256: 250 per-frame NV12 hashes of the vendored HEVC vector from libavcodec's software decoder, cross-checked frame-for-frame between two independent FFmpeg builds (8.0.1 in pf-lxcheck2, 8.1.1 from Homebrew) — the sibling of the H.264 goldens, ready for WP-2's parity leg. Gates: fmt clean; pf-bitstream clippy clean, 69 tests green (the replay stays ignored in normal runs). --- crates/pf-bitstream/tests/corpus_replay.rs | 232 +++++++++++++++ .../tests/data/test-25fps-h265.nv12.sha256 | 268 ++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 crates/pf-bitstream/tests/corpus_replay.rs create mode 100644 crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 diff --git a/crates/pf-bitstream/tests/corpus_replay.rs b/crates/pf-bitstream/tests/corpus_replay.rs new file mode 100644 index 00000000..5a759767 --- /dev/null +++ b/crates/pf-bitstream/tests/corpus_replay.rs @@ -0,0 +1,232 @@ +//! Corpus replay: walk a captured real-host stream through the planners. +//! +//! The M0 capture hook (`PUNKTFUNK_DUMP_VIDEO=` on any desktop client) writes +//! the exact decoder input of a live session — `au-.` plus an `.idx` +//! sidecar carrying `offset len flags complete` per AU. This harness feeds those AUs +//! back through [`pf_bitstream::h264::H264Planner`] / [`pf_bitstream::h265::H265Planner`] +//! and asserts the planner survives a REAL host stream: every AU plans (bar the +//! deliberate skips), no panic, and the warnings are only the ones a clean capture may +//! legitimately produce. +//! +//! Why this exists separately from the vendored conformance vectors: those prove we +//! match the spec's own test streams, and the on-glass sessions prove the whole pipe — +//! but between the two sits "does the planner handle what OUR five host encoder +//! families actually emit", which is the question the corpus was captured to answer. +//! For HEVC this is the ONLY pre-wiring validation against real host output (the +//! client's HEVC rung is still being built), so it runs long before M3 finishes. +//! +//! Ignored by default: captures are hundreds of megabytes and live outside the repo. +//! Run one explicitly — +//! +//! ```text +//! PF_CORPUS=/path/to/au-1785970273.h265 \ +//! cargo test -p pf-bitstream --test corpus_replay -- --ignored --nocapture +//! ``` +//! +//! The `.idx` sidecar is found next to the data file (`.idx`); the codec comes +//! from the extension, matching the capture hook's own naming convention. + +use std::path::Path; +use std::path::PathBuf; + +/// One captured access unit: its byte range in the data file, plus the wire bits the +/// byte stream itself cannot carry. +struct CapturedAu { + offset: usize, + len: usize, + /// The wire `flags` byte (`USER_FLAG_*`) — kept for the RFI/intra-refresh legs, + /// which discriminate on it. + _flags: u32, + complete: bool, +} + +/// Parse the `.idx` sidecar: one `offset len flags complete` line per AU, `#` comments +/// and blank lines skipped (the hook writes none today, but a hand-trimmed corpus file +/// is a thing a human will produce). +/// +/// A malformed FINAL line is dropped with a note instead of failing: ending a capture +/// means killing the client, so the last buffered line is routinely half-written (the +/// hook's own docs call a truncated last AU acceptable). Anywhere else a malformed line +/// means the sidecar is corrupt and the run must not quietly replay a subset. +fn read_index(path: &Path) -> Vec { + let text = std::fs::read_to_string(path) + .unwrap_or_else(|e| panic!("cannot read the index sidecar {}: {e}", path.display())); + let lines: Vec<&str> = text + .lines() + .filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#')) + .collect(); + let last = lines.len().saturating_sub(1); + let mut out = Vec::with_capacity(lines.len()); + for (n, line) in lines.iter().enumerate() { + match parse_index_line(line) { + Some(au) => out.push(au), + None if n == last => { + println!("note: dropping a truncated final index line ({line:?})"); + } + None => panic!("index line {n} is malformed: {line:?}"), + } + } + out +} + +/// One `offset len flags complete` line, or `None` when it is not four parsable fields. +fn parse_index_line(line: &str) -> Option { + let mut it = line.split_whitespace(); + let num = |raw: &str| -> Option { + match raw.strip_prefix("0x") { + Some(hex) => u64::from_str_radix(hex, 16).ok(), + None => raw.parse().ok(), + } + }; + let offset = num(it.next()?)?; + let len = num(it.next()?)?; + let flags = num(it.next()?)?; + let complete = num(it.next()?)?; + Some(CapturedAu { + offset: offset as usize, + len: len as usize, + _flags: flags as u32, + complete: complete != 0, + }) +} + +/// The capture named by `PF_CORPUS`, or `None` when the variable is unset. +fn corpus_from_env() -> Option<(PathBuf, Vec, Vec)> { + let path = PathBuf::from(std::env::var_os("PF_CORPUS")?); + let data = std::fs::read(&path) + .unwrap_or_else(|e| panic!("cannot read the capture {}: {e}", path.display())); + let mut idx = path.clone().into_os_string(); + idx.push(".idx"); + let mut index = read_index(Path::new(&idx)); + // Same truncation story on the data side: the final AU's bytes may not all have + // reached the file before the client died. Drop AUs the data cannot cover — but + // only from the tail, so a short file can never silently hide a middle gap. + let covered = index + .iter() + .take_while(|au| au.offset.saturating_add(au.len) <= data.len()) + .count(); + if covered < index.len() { + println!( + "note: dropping {} index entr{} past the end of the data file (truncated capture)", + index.len() - covered, + if index.len() - covered == 1 { + "y" + } else { + "ies" + }, + ); + index.truncate(covered); + } + assert!(!index.is_empty(), "the capture's index is empty"); + Some((path, data, index)) +} + +/// Per-AU outcome tally — what the run reports and asserts on. +#[derive(Default)] +struct Tally { + planned: usize, + skipped: usize, + errors: Vec, + warnings: Vec, + partial: usize, +} + +impl Tally { + /// A clean capture of a healthy session must plan every complete AU. Errors are + /// hard failures; warnings are printed and capped — `MissingReference` on a stream + /// that never lost a packet would mean the planner invented a gap. + fn assert_clean(&self, total: usize) { + println!( + "planned {} / skipped {} / partial-AUs-ignored {} / errors {} / warnings {} \ + (of {total} captured AUs)", + self.planned, + self.skipped, + self.partial, + self.errors.len(), + self.warnings.len(), + ); + for w in self.warnings.iter().take(20) { + println!(" warning: {w}"); + } + for e in self.errors.iter().take(20) { + println!(" ERROR: {e}"); + } + assert!( + self.errors.is_empty(), + "{} AUs failed to plan — first: {}", + self.errors.len(), + self.errors[0], + ); + assert!( + self.warnings.is_empty(), + "{} planner warnings on a clean capture — first: {}", + self.warnings.len(), + self.warnings[0], + ); + assert!(self.planned > 0, "no AU planned at all"); + } +} + +#[test] +#[ignore = "needs a capture: PF_CORPUS= (see the module docs)"] +fn a_captured_host_stream_replays_through_the_planner() { + let Some((path, data, index)) = corpus_from_env() else { + panic!("PF_CORPUS is unset — see the module docs for the invocation"); + }; + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default() + .to_owned(); + println!( + "replaying {} ({} bytes, {} AUs, codec {ext})", + path.display(), + data.len(), + index.len(), + ); + + let mut tally = Tally::default(); + // The planners take one COMPLETE AU. A partial AU (the wire's shard split) is the + // pump's business, not the planner's — count and skip rather than feed a fragment. + let complete: Vec<&CapturedAu> = index.iter().filter(|au| au.complete).collect(); + tally.partial = index.len() - complete.len(); + + match ext.as_str() { + "h265" => { + let mut planner = pf_bitstream::h265::H265Planner::new(); + for (i, au) in complete.iter().enumerate() { + let bytes = &data[au.offset..au.offset + au.len]; + match planner.plan_au(bytes) { + Ok(plan) => { + tally.planned += 1; + for w in &plan.warnings { + tally.warnings.push(format!("AU {i}: {w:?}")); + } + } + // The spec's own skip (8.1.3): decode nothing, show nothing, the + // stream is healthy — never an error (the WP-2 contract note). + Err(pf_bitstream::h265::PlanError::RaslSkipped { .. }) => tally.skipped += 1, + Err(e) => tally.errors.push(format!("AU {i}: {e}")), + } + } + } + "h264" => { + let mut planner = pf_bitstream::h264::H264Planner::new(); + for (i, au) in complete.iter().enumerate() { + let bytes = &data[au.offset..au.offset + au.len]; + match planner.plan_au(bytes) { + Ok(plan) => { + tally.planned += 1; + for w in &plan.warnings { + tally.warnings.push(format!("AU {i}: {w:?}")); + } + } + Err(e) => tally.errors.push(format!("AU {i}: {e}")), + } + } + } + other => panic!("no planner for a .{other} capture (h264/h265 only today)"), + } + + tally.assert_clean(index.len()); +} diff --git a/crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 b/crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 new file mode 100644 index 00000000..4863aaf4 --- /dev/null +++ b/crates/pf-vkdecode/tests/data/test-25fps-h265.nv12.sha256 @@ -0,0 +1,268 @@ +# SHA-256 per decoded frame of test-25fps.h265, DISPLAY order — 250 frames. +# Each frame is the 320x240 picture as tightly packed NV12: +# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame. +# (This vector carries no conformance window — coded size IS display size.) +# +# Generated from libavcodec's software decoder (H.265 decoding is exactly +# specified — every conformant decoder is bit-identical), 2026-08-06, and +# CROSS-CHECKED between two independent FFmpeg builds that agreed on all 250 +# frames: 8.0.1-3ubuntu2 inside the pf-lxcheck2 image (docker run --rm +# --platform linux/amd64, ffmpeg via `apt-get install -y ffmpeg`) and 8.1.1 +# from Homebrew on macOS/arm64: +# +# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265 \ +# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv +# # then split ref.yuv into 115200-byte frames and sha256 each +# +# Sibling of data/test-25fps.nv12.sha256 (the H.264 goldens) and consumed the +# same way by the HEVC parity test. +ef4900effa72cbc00cff38938bc558cc5d70b31ca268aaf3c01c4268a9d8066c +fdbae4473d24c3adcef59a51c1449304c2fa271412622efceeba5ee7437b5529 +9aab5493a8fe3dfbd3f1eab89bf67a4964998bf8f1229c672ddf2d0d5fe26edb +52797a59351b0a02069a3d8b2022ad7e06be4e03a7ab9483e09c0b03e4dbfec9 +9c68407853977ddd95dda04b77676e983fd7ad4665bbbe65497ea5e91f5b19d9 +1d7e6adc6a225e1bcd2c5cc598c0342472edd2d7dcc162c38872fc665b57f3d5 +4633e66a0ec77f26ab955bea209b1e976947ae87d8405e4a70be2d8e6ab69625 +6cce1546ee5c1d414353a990aeb6cd3035199196cc6bd527d1f7985247c30cfc +6e5eb5c2286156d4c7406c9bba159348618799fdf2d46d470ad4f0fe2c6671dc +25b0af55a492eaa503349ab45f1d6449df832361ac60d2910eade5380c5b0789 +dcae9606d93f9405c95e5a03699beeae1442240303df2d457bb3b5fd5e2cbd18 +d7bcf9616f062b9cbdaa076db0aa8fe0e60ef83b51c1336276183fa4d6f2f0cb +003e43b5d1e731ae234146763c1f65b4c4e2a67fdab3d938b7ba442be360a520 +3bbf1a62250deeae73fdf77ff34c2c850886557bc79815a2c968a5d8968a67ba +d8fff092b581e7807b77df684e74f97013b6c69d4610ad17f08855585c3ef5d6 +52d3d01f8f147d8574295859ba1f7437ff11d57eafb5c5325b55c7533abf922e +e75eef989830cbd6c8dc3957fcd96e1061f861c5f12eaefadd21f600ce0998a0 +9ffd2496189a6384eddda2e752a719e27c6c69e5fc74330b75fa7e4c928cfd55 +ff304806867abf9d6d683c0d3bce95473cbad3252e815463e9eb5a1b5fabfd6d +9320a5e1fd33ec1c8b3d2964af8050bbcec219b3287b83c2cbe6f61e8d4f3cd7 +9816da3390b510218fc8605398f9cee8d0c8561e2933f2e2a1f771023a7116ea +198b078fda1f6f1402a53e8c73b99544abb8c4091efab68e7221af73732fc10c +fb13ab23a5e012e4ff0cdbba1af7261a636940831c1cbbe1065fdd4eeb4754f4 +73fca4a423291a3ff6c787a7660085732a687d7e061dff235a29d83ceb96e95b +195ce227acdcfa1a4cd4fe35eff3291ebdd5f3fe7545509e32e17c744eb98b4a +706aee7736f9c599b48dcd156cb27faaaae51f3fe85b14dc12402207033aed17 +e061a0ab22441f9b2be0fd111ca67248ea40bb3f5d3b280b3c52648eed8444b1 +93be6f5a81550ecca250ee0c145e5c4135d05ef0f4a78095f94e85070cae9db9 +465723432c68de41e96fc32e562812343a9fdecd53b9699fcc6770b5f9e4b002 +874b39b128db5b47160cf0d8bcc0040063e23dc5b7f0172a8149f78e7ab57195 +3cee067706084dd8740ba200e3eb1955c93e1df4aaecc298453560e61f658271 +7ea42e3f7c7a6488057d4cb7db1f7dd61f505cdcf8e71bcaa33eb00b734018bf +55b00704e979ac7bf2e4961dfe12f1ef5c559a421e24e3243f943a013e679fe0 +e91f2096b0bde6bef5762f2765d2ce30c77f9e1a9adc151bd07277fb715c8131 +08d1391f8876e67cd086d79a703401da70abdd37e3522445830e40cd820cf3ba +87531fd1641407d8eee5888068c9655920976deaaace01b0c6b6766dace53d62 +1993c8add817ed50882c865eff2c82f93f1c8f050e1ee3171d9518e3a6ac0563 +f682098c0cfa38af29ecb40572aae48c4663e11c5ba9ae256871a1fc62111dd7 +dca4d5cad38a35d15ecd4ef02e2a2effc6c103ac4f27ed61518416bfb8084f72 +33a6d608d4dcb2cb47d648ff4637b62e65b4a769093ba0bbf39ef151e174cf5f +814d05ff585e21676fe08a867ca4593c65870b81d5bb7ae51d8fb795371bf9e5 +91339266713bb79af8d547ad888a010b7ad56debc938e4ecd9f3542a8d148e40 +945258f865bb6b795d06609d57ac77ee5d863bf19fb35785ac7578d46e229cb8 +2b2619f2814b08675b263e38ae8a50d816fca71d6f014f6058cea2b4dae74aee +89b8983416633c225519b3621bf65601a0f6c0f46de5ca3250a9b0c34edff73f +0a007d2eacaf06f629ef854450fca7d83d6f6e1bab60a1145466dc157082f877 +e8550a8a431f623ccfc8457c4f171a6c9bfdc1c67807fe1fb45cf4bdbc14f7d1 +aafa9b366c4d560b9994366b1ec3a2f71c861191316b84e6b46ba7ddf79c19ee +4ec7b250bb49ec17b34a090f36f8cd07692829dcba1ee939a4526e89366ba1a5 +c00e70ec1b43cea1b3d2909d61cd53239f428085b6769913307c9006bfeae83c +b8744028170518a8542d4f37ff05990230ea1ab87d71896b0fb41866b3859e57 +cc9b833a5725bae1c6e9d198c148ef45fced9b34a36fcf043cfd2774baf9ac55 +81f3666280e765cc8641fcfb0a65fac70175e18c7ab701534a5499ffe3fa8543 +f92faf17adfb611afa432c2c4f3c192ca24246a9613601ca109bcec0d8a18c56 +6acda837d6691b80b11c9c83fb41108b0aaefd1a90b5bdae28429542ae998f5c +94db3458a1fac5e9cd5a9ec07de06c659a20051b80bf71b2435d174604e502bd +cdbb03c8d67aeef96c7073346a87b1a41e866c2fec75a5003ab43d10ba953457 +2f05ec023ce09ce00b3fdd277e0da4aabe1e1a073e9612632fb5f5c95103caa7 +5587ef3923591036daf4d471a8a131d04fff2a6612e2e7788ac4662a8a3e16eb +19c7c1534c40e8a4dd7a6461fcd5d8695951dca24d7d2923b5ca12cb4160afe1 +95bb6aefb8ac74e52e3ef64e224a64eecb08d82ff3449d1cc1b8b44888a2ca38 +1278e669f16f311582ea9456e7361c57d4b4748c9902c780c4d9d62c7e603f1c +0d77145396334c2f3008b72441d594f7c3e110da95aef11d6bede46f49c79be9 +099ea10831ceda528df779d1c7b679cdc1d251f10cfc39f4950f7fb3a3cebd36 +8cb5a50ee01d37f8501884be3cd5239f5e57892901afedc4ddc3a9fecf435b0a +5c4f777977b786ab5e80f6bd7ea9880aae7fd8d8cf9e4d79016020d4fc916c22 +d635fab89355529ee6661f8e867e5361bd28773422f8478939dd806162292346 +baebab9b93c6dbb63ea19468538d9663a2f9fef5bae21dba46161454ca05c770 +22f4c44eec9c70d32b05cf0eccfbdf5b477b0f93bc61a17f9b5b4c5a7dbc2ebf +fecadfb3ec25bd00929059e9849da387ca0cee8eec9f0792128f3256eddd5bd7 +48065431ce0b3f8f0ad5f77fc30fa29eb961b1ba5e6a97c0bf41c5d3c0ea8518 +b575ccc73436b7937f98ec1847fd54d7c7a3a086d08adcff9d226613efd5bac6 +be5d094c2a2ab5feb6c988c68e9f2d6eee73bc69e8c51ed24786325d185ad5f6 +6c16634620280926af758f013b66c2a75a79ea67a90184965ea1bf91b9f51696 +0b63b9eedc87d047474f9c46e71dee75ca1e3a5db5cb0c9d7ea0627fa8c82876 +02052e7622fa96d6a0696f5c27d632ee49caedc5ac908a5c9f6a8d6d475be6e3 +ceeff7e73cca36a48a775f2e6b773d7c968c20de18bef4460dca582e6ba4b894 +693663aaf34e534a4f6d2ae7846aa26aa1d73b4bd9f854dd70bdb6a41b0cbb65 +474a10f6eefba5c7d16efd99fee59e0b762380b0f3acbb91e76bdc18a6dc9c02 +4c005f6dbb86ba5ef0001a551af8b4e4aa996e5f27c23658ea3002d319e3c53a +9968c5c0966a246dad5e0b49668d3d1caccc12a7722aaff677a6ad63c32b961e +a1d57c958b92e6a8d557893c62cea08d5959dd34a1421bf1fba02adf7acf0244 +f885bb6f13840be75c88223d07105f0ccbd4aa678ac2e76ca347739b0e3158b0 +644526f73200746ed78f8d0a35bacf486b17c6bb890f58e12c433ccdad59a37f +2beef1de03e18970f81792b0d247fdc9112d603dca852507b48d3585c86a7fa6 +b76fe733ccb80eb233ccd263dbfe264b6c298a93a3847159ab19fd8d9566d63d +cc7cfa0dfd02fec28ddc9504cf0e4a2656c2b3e7e2f13207b9979f9930408ea6 +fa6546a8828e3e3c75b1a60c0681db322d521fd4dd3fc9b005ccfb42d58fd9ba +d1b2527753951c11ff69e4db8323c058e40f411bfa4e23915f5675e30cd43b77 +5eaf026663142af2ba32628a4589aaa143977ecba28181730172099c6f749abb +9d18abd96a6548cb643b39a2499b1f718713dbd92f66f35392fa0f9fcdf4a81b +38ce5c4261c740ab054dac484af6afe58bf5eacd95fd322113cf532f362e0ea2 +55852ee3a2f92db0bdfc64c2693ff5a43ed7348a6ea947aaf9646b964c1842e7 +ba7488d71bf856cebde72d96245a582efae3d3392dc75615fdce2146b123cb0f +3e4c55e47febd86861cf4a7b56cb2e6fdcb1d9542d521ab04f57643d844d2f81 +8df056cf2f0d1e13c1d1a419251b09a9e1edd164e5c9ace7525be2c08d7cb84f +2e7fc74a622da56c8ad17d66f7fa2bd5a466dfe4147db80b7a58dbc93158a0ea +ffac7a103ab051aad9ff90fa43f9d98c149aaa6057d59660a70256bc9ab3951a +3725bd25b394076327a8f6ff010fb9e4b7662d67b48c80693997d434eb541399 +fd807e07cd0f003ac3d834487bd7526b000d3ffd79295ba92fc10caf3e5bd6ad +071469b129dc9ef55e67f773ad5c2ef6498b728169e98910dc5923b4181e3ad2 +512ced1859982b5bbb58f48a5ae8006c56bd28e165f0a761c38bf214d2636983 +8a663c0abe78913974d29f4380b7addc46612d55fb431c11042bb7985f22d501 +92ea5506c94d885aedb9dfa7f884a43ed8289b49542b67861e576cfea4d62b11 +a29dde155a03511735f9370ce23b95bfd98868434ddf6e9a3669c2edd0ad1d8f +1688fd2fe7b5f7e4cfcc40d3afff903bd0a5cb8536c0d7da4d53b90544b03227 +96f593ab7339f0cb02bec07b6f5bcc11d2494ced35a90d4037c961a2e284d914 +9450d79524043cc3f2afe0dda19b6a5a06de7128ad9826802b798255f72aaf00 +4dee54c01448ed0595fe1c0108c97031c48de96ea425c8f8c2e5b3382e862a3e +24bb1d429aee55c9d2c719777d15a3e8116fd2074a569a5e44a8d5de6d11fe8f +fe0255625438d6403fd663f60eab05ff990780054eb530dbfaa3552a978cfdc5 +452961eb29a947dd2818409c8d5e283ebf47281eebeb9c368164b22dadb315a5 +e49ad550b30140b2b99b6edd0e1a7873f3ba0145829037c2490467ddfcfc439a +f02dd0ac342330223e9c0ae56ed6a9efc3a0c69d8bb4c036943dd16f654f10bf +60352cf761e2cd8ba2cc0025c45e715a8719fabe78d5de7a382da4b648704ea7 +3b79506fc18e01a1168f880712c539f0a520712dd62d2f824b6b754ce034f1f2 +3c16234ed4606128fca31196a81f769ac69a4261aa176bf8c8d86b8699177926 +6a98fcfcef5d1ed813ba3f2f66f5b1bd11729430187a7366dcbe9617f2914646 +5683c679c3c0fb24b740ef4582c787591f10dcffc9e46037422829f5b58a1501 +9b4c0ca82d8440b630a569a8d6525384beb26ca7d54b7a1ddb1b83a00e134701 +5693915c011d2ad738b84c8a04cfb8081006b8835ec655e76d829e5dbcbe0688 +1ddbeb8cbe7d0d8ce61a94e3ca35730a07e59bdbb692cc82dfa3ea0ff9ba5979 +ca10afdd882627e9a6cb5d389c71fa015cb0015124c7445295ac42660a01c491 +7aa406047f3fdc2a07d6d228b30535734689c99d93e8c5c7865044187ca0d3b8 +8e3f8d26612b58f985ed778f1ad5f6cca0be49acee5b7d4d09ab2b886a3327bd +9edb22ab753a8cb66392d301144df20c7e67e3afe6bc9d1c3143087ecf26120f +fc4d040db9172642d710b4edabca7e5444fdc9099f9867b5942e200da1d6373a +1cfd8b95c000cc30892061db6dac6323f2f7a654c30d0ed024368997ac65e673 +f9ebda67091c2b26e7205bdab431299d982dace99cedfec13c80cfd3195286c0 +5cea0e5febf079803e85b57bf45d3d563d0e8fa1e45844af18609077497fbd3c +2c4c74504660b5222f2135407805000fe1a41df48c47b28685e57020c6d21061 +d49cffec62c461a04292556bb21f78861556b058db006143696e53cee077d26b +9d7ad96746eef142fec236232c0d275943a5258a40fe3fa384aaa5614856cbc2 +958cde369e11df3a7bc534ac549fc11b4a6e325fc02942f49c68c47e99ed65bc +a05043d7198834dd8da3d544dd7c11d3f03a3389a422fd0a0be732824b587047 +700a4641d8ee0a7ab2b0a09421755ee6223ef1f7b6436a96eaa479140361b40b +16d487df87dc05fa10373f139558597f0c82b9a22828de0ec462fae72ac09124 +73a8de0a8f0652405aad48cb13765767c80286ef190dfc1a10234fae0ee0dc71 +19f30498e93062ddf30e284bb44b9c6b6ee5c2ef6e143d40cf7dd76b669bf670 +93de878f03e69fce3ec00963df3bb941713bf225b665b0baede46e4a9559b567 +fc55a32f7fc41925a5bdf7fcf34513b90b6acc326771fd77db7fc0f4572db395 +e84bffc862d0d017903ec43414b0659b9720d0a4d646be3d1576a168a4333ab3 +ef655d0f2ee29f23409d1a8689710e024aae636c1ecfe8bc1915774a94732f8d +95c63e9092ba3a5887e9ba2044aea96315b25e55af2fbadbad50345fb98629fa +15501a54f24943a19a663263c5459d31c45441147cd58eba4935ab67d1bed291 +bedcf37b2101e06e9f76f85df28e70574bff094ddee96fb3b4733da63551e22a +e06487f3691cb56df61d13e838d62a403e7cf4cbf399c89e98a7d04597e91653 +483596c621e2646ffe668743eb0e55269bc7dde6188ffb80c086b215d73a9717 +a84fcc692a5ae998f082fb21e429076d59c48fa6880309bbe692e6f0b5c0ab86 +4aace8b7ea4891aa80f8df369689e0dd587d88d55160d40ff1fc03dd4fb7fe44 +b0b13ef2e6684489fe0af1ddcc2dc2e04e833d372b7c4838d1cd173da993265d +6ca96a7be457ba2bf06aba287fdad92bf356d31723d392e654ff352d480b5da0 +69554f33448ca613f466adbb14748ce2001518c861397100579340b9d9c71dee +baebb6de906b34a88a890274e491ffe00ce28b003b0ef1331c484740c036a30d +841a7b7efd05b4f8aadf6bb0074067a64b118c20505f317babcdebcdac05b9aa +d2eeff202d639a4c0ff451caad9cc7a7f4bb8aa62e6659671fadc4ba85a7eb29 +4ebc9035f1db8feb6e56f47a180b4108ee374c7442ea5cb2ba4fa59427d88fc6 +b7e2988ff896ed2618a17766dff44839b23b934108a1d75948ed0826c8129d42 +9eb4b2490eabc51f7fcc75c5aad419208238ed6807ff109ec02cf0f6ac1e12dc +4ba2d5fc6b9b27c53b4217b04f70dc219073a9c1cb0863e040d9973c88899084 +e26701bcba0f2014ca93eee23893e020bf681e071c9e62d0fb75619de7de715c +ec829d9898ba7eccb51af41e5412255bfe93e61137f56a21d4999911fc154bfe +ad47b88c0fc7750dd1a5cc27cf134638a9270342e957aef81e517a5a519933fd +1d39f1ebd301526344a29c8d07049a389ce157e8a0184fb34e38ad3c2a851afd +20d4699ce3a43154a62ef5cbf8f14bc47cfc344eff92ec78e4e6108d2827ad33 +79ceadfa9d591934dc9d87125772ae7cf3a0f26fe91ee77f27eb11a23924981f +3d481e4dcd5f0e849323822d687f4cbc7570474b985251fc13cb019ac5f72a81 +80a680a0054e1b0968a3ade98a8ccb42789082584d3a72917d2a1857b348b5e6 +fd031ad921c6dad12473e6fa148c74e05660e98be91cb68d3c5b43da423d06d5 +7f5fa38b7ce6401ff353a9b680fccadc138af24cd66369a05a796c363bcdeb45 +8a6ab1effe3736620a7d21ff943e7c50bcd244d9ebd26dd0f613c86b1e866ad2 +8020cb196e1830ce33c5242cebae2c13ca64e36b0a1186cda71b3b19fcdcddef +8c8ad9f8f7fd75798017937e117699485711220f2b529f0b4e62c1765461538c +66cad5c567e55c73b672f710a95c05825e26786ba1c69551f0d1c6ef92887d70 +02379f8b49c069e1a14e4fc371b84aa46e0589d6b77050a51fa888b20de13a7f +f7bc1ca4d9c8a3ac7dd2c53f3829c459bf656fef033a1c2e07942e573f1488f8 +fafd7c0ef34504324578f48f22bdcca6b87d3a57a80ac6a9359b9559ba1ba599 +e0f5d057bc2ea677d16506f272387801e5717afad3c27d29c56e6f491496d399 +951dbfd636ec1d0a164350cad1f0a69f097ae28447520e6d3ce4dd18cb7ce33e +b1707faa90c8d2853c98f3deb845283b119a3f626ff8733d1ca23367b617e24e +6835f35622b8a7f0a3393cc95aae42dd81d51cc31de0fd70aa0a2c4d9f7dc540 +6038a51c740da0a594613a563e0290493e1cea0f45b98334711137ecb309ce1d +9426f1fb57ce97b8a50cdfc98da236cd6c12404e0d9a27dbecfe53bd4e70ecc8 +55d02d7484a0e86dd43b28722b91ee46e47af72cddb478692b0569cf0ea19f8d +48a809c673c51d8acd8c8de0877bf5addd82ebd7ae39260dee46627b99e5f35e +61d251ca5cd28071a241e6e31dd1cca9d6c92aeab9bfb705c90fc02fa59d0721 +2e129f22a54b5a80cd9993eb6b10c0a8f3eb7abe2b47f5d2cc64c9365a083864 +c7acbe0e490196575368ffe3cdb08a12cb99466c0c77423fd1592033591e04ba +193d41b5c3ef5c85851f4ab1b3b5fb8a8f90575e2377362e89e35915de9166c3 +eb4c463ca22eac43632f8f5048343457e9bb0bbce83a67c4f3c34a74341622a1 +daff5bbe42272e24b29056ea9ab29b5dd70b9aa985370989ff1502d20b715890 +22b400207ffd843e7aec2ec2cbd6d53159e7252f1c60314ad5164f903862972b +d4f3bf876293ee9de770ae8a940458d75ed0f392a7fc93682223cee31d257d32 +a0ed984f000cf909cba32fddc9c9ac78c626105f2c0bc8abbf3f3f50d312a342 +825ca691eeaf5c2373892742050620e26ce1c1e81fb88c8ef089a1fbabb66448 +08e2a544c9756812b03513048e7ae109e9989aa87facd534ad633c15e440db49 +538d67388d4f47b8ee58da756dfb913f2762610044fc375849a4aeddd9cf8adc +7b11c5921e69b8544ad1fa6ff1f7da57912f1473392a64312b0132e657bf788e +87b0b1cb7b95066f491834c1f3f01afb6bc9e6e95a8a3a7b4cd3177ff0d52f4f +a6746ac64f664e61e3d1d041fc71ca5379c831925b714461b3203187e44a2542 +a88bdd638069672b79a708aeea9a12ab17fd7baebfc898182d4df64404dce9ca +0db7d0fb2b4262b93d1ec21bf00a428fafc96fb410cf1d5d2c30a8cefd374e93 +8e2218f82bc6b653705ff81ff03dc98dc90dd7249183521e1dae62429fe6e4ad +2d48037c916139c029f0d9ff9bb47ecb5dd4c0e3af9ce746077cb4a16759c62f +a03d1c762fea0edb027741537fbdad74b2aee95828218c8c6358ebe019f14782 +711ea5dd1cd0884c949066bc35af77e89a6760fc7812feaf34ff34c14b6cd124 +1802816711243b32c875a8b0c5fe04212f9dac427dfaa04a4ae614111cc9d490 +11421e2188d4b46d484edc1f346f22fefeba6ed504952fa53b33878512b7f364 +a54098f99bf02a7a143ae21a0e05809ee66ba2f712fa9a270c307fde43587f14 +0c3fdc1a5c92bba34cc7bd69f84771e7a979601e71428b9e8ff91e7a104552ff +023b3bf5fac6a8518ee80de4b3a41e443cee431eaa6e701b839b2ab5b42420b5 +cc073fa309af6fd9023c642316785bbd56d49f4ba60f96248a8991d3287455f3 +a24b3cf2b9e1c2639e8dc4a33dcc4f652afe949e8dfe50d3dbe6fb075c63e5ef +454e182ce3b29aa1942470b0a223d6070dfdfad119fbd9cd86d52d34d0261bd1 +14287adcbbd45689d25c04cc29edb30f8f2bc8e40499436fa09b33558a8f3c5e +ae957a764471b46c353396802418c1a13f3701b7847ed3749bcb619a95ec5546 +500832b21119374cec8b831a06969cb0534892707b3c8b8d49354bc2c9e87262 +29a45c4d1a1240f45efb271b7710418501030ece1a526557364f65591d34daf8 +2ecc2c67e41643ee4d3a1b1c27d2d608396b68de4293d2df2e061faca97456e6 +051f0a7a60641592f2a83846c157bb42538bf0be3f31b27e24f6c69ca6575485 +c108938b16d30b2a13bbaa2fea82ddecefd85b3c0e52a0a200f63a7c5c6526c9 +5b5c1d60dcef4e90866a824a1dbf3e0cb67eb95661dee5f17aaa146d04c99c8e +eb8517229cccf5d0a5f3f43f21ef18ca68520684f4761fed62894555e05fb132 +227566441065679a17fcaae3a15b019606f3ea941b0b78e2fbcff48c0de253ea +61618f5db64098bfbfe1f4a6e05ff647c703a00b74854bc4a5a60dd1672a7e66 +44add97c02f2d128a3b15363ccf61c126438aa96c826b2c0577402fa56de3cb7 +00feb43366991297b770fe2929bde6d1844e6b984662db62323138662c9b51db +43dac961a766a410c13eb45ff6422d6f9fd69944e1d81801b19bb38a2f30fb54 +e8deb99a10dcf354e4065c587c51feaba35d45b6a3cd3333a5d8657ce0e7bc34 +f0729cfcd090b1ad620934a99d06adc5705a4bd2d2d57ac2e82a35f5e030eb1d +9cb217fd3190461954921f1b5be6e6d7337cd694ad30d7a3e10f672b2f74a717 +346cf7fd86aab5ab941b926a243c6aa7b65fdca52aaddfd6884cc1fd7e0f4a2e +d8ce9c4921d6fdd4c57dc64a250c2dc886e74e9bff336b8c7030ba4a539ff79a +eb67d21bf9fec6d9ee9af47b3dfe95a44c201836d17e7e08a3bfe561908b7008 +bdb306831f8bbcd7e675f7db95ef50d66ecff44dabdfe6e89972cf016b915b6b +e3d1aed962e48afacd57007efcfcafb0b4d245ae4c579676d98c1b21855fbf9d +c092a99f788b140c59d0288560c53c6522cffcaa9223c2ce88b8fb8a7ad5ee45 +6d0dc8953f306f6913d3be67c54a113eee14ca9600b7d6b5cd99f2ea18cf4984 +94bd0d5ee0cc10c95c836337733f1730be33151383d7946afbc24e2e2fabd346 +99ffdc3d1b432890b662126c09dc56a45c6491b580dd5bc4b2a7f464fd7b00ed +fb8d38ac9ebfb8f196a6549161931d723151d3aa180f77fe9130e059e12d2481 +fca6ae50a370e226998e785ec9c5b7dcfcb23263ee7bb621da05eaa6fcb806e7 +f3d0c20bd10b0b2fae3ea4bb4180ae2174310a283c03f5c7733ac937a07198fc +f4cdac0c218c483533fdd15627cb3c93296b714159bbb1594c3c3ea59ba09185 +db3b2227ac0212da4c2a01ba24c41aa1a075e42117d70bd220b8f03c27f6602f +9561dc8fad0e5e16c1a035da4edaa1c8255cd36340a0d3ee2731ffa65be2037d +53dc9c6bc9462c30449f4df70c19981d5d144895518bee4258934399f91fc181 +ab0bf02e7debd2d7fc43f9e0d3cf5d3c816a9c10398b3336eb17cf6c3ef20f3f +d1666cdfb8645f4f2e5f554a4036e789dc98eadc0d06bf99c7daa4203a0a3e77 +30044bd22f2ed02193191fae5ece9a623c7c9b22442fc2f2274da08a5708f0d6