| // File: basisu_xbc7_decoder.inl | |
| // XBC7 decoder implementation. #included at the END of basisu_transcoder.cpp | |
| // (the transcoder stays a single .cpp). Declarations live in | |
| // basisu_xbc7_decoder.h. Decoding REQUIRES zstd (BASISD_SUPPORT_KTX2_ZSTD); when | |
| // that is disabled the public entry points fail with an error. | |
| namespace basist { | |
| namespace xbc7 { | |
| bool eval_weight_predictor( | |
| uint32_t cand_index, uint32_t amp_code, | |
| uint32_t bx, uint32_t by, uint32_t num_blocks_x, | |
| const tile_bounds& tile, // every block reference is clamped to this AABB (encoder: the job's tile; decoder: whole image) | |
| const vector2D<basist::bc7u::log_bc7_block>& log_blks, | |
| uint32_t p, int pOut_preds[16]) | |
| { | |
| BASISU_NOTE_UNUSED(num_blocks_x); // bounds now come from the tile | |
| assert(cand_index != cCandAbsolute); | |
| assert(cand_index < cTotalCandidates); | |
| assert(amp_code < 4); | |
| assert(tile.contains((int)bx, (int)by)); | |
| auto fetch_w = [](const basist::bc7u::log_bc7_block* pBlk, uint32_t plane, uint32_t w) -> int | |
| { | |
| const uint32_t sp = pBlk->is_dual_plane() ? plane : 0; | |
| return basist::bc7u::dequant_weight(pBlk->m_weights[sp][w], pBlk->m_weight_bits[sp]); | |
| }; | |
| const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr; | |
| const basist::bc7u::log_bc7_block* pRight_diag_log_blk = tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr; | |
| const basist::bc7u::log_bc7_block* pUp_log_blk = tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr; | |
| const basist::bc7u::log_bc7_block* pLeft_log_blk = tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr; | |
| const basist::bc7u::log_bc7_block* pCand_log_blk = nullptr; | |
| if (cand_index == cCandAbsolute) | |
| { | |
| } | |
| else if (cand_index >= cCandFirstXYDelta) | |
| { | |
| // generic causal block reference (copy); subsumes the old | |
| // left/up/left-diag/right-diag copy candidates | |
| const xbc7_xy_delta& delta = g_xbc7_xy_deltas[cand_index - cCandFirstXYDelta]; | |
| const int nx = (int)bx + delta.m_dx; | |
| const int ny = (int)by + delta.m_dy; | |
| if (tile.contains(nx, ny)) | |
| pCand_log_blk = &log_blks(nx, ny); | |
| if (!pCand_log_blk) | |
| return false; | |
| } | |
| else | |
| { | |
| if (cand_index == cCandLeftEdge) | |
| pCand_log_blk = pLeft_log_blk; // left edge | |
| else if (cand_index == cCandUpperEdge) | |
| pCand_log_blk = pUp_log_blk; // upper edge | |
| else if (cand_index == cCandLUBlend) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // left+upper edge blend | |
| else if (cand_index == cCandReflectLeft) | |
| pCand_log_blk = pLeft_log_blk; // reflect left | |
| else if (cand_index == cCandReflectUpper) | |
| pCand_log_blk = pUp_log_blk; // reflect upper | |
| else if (cand_index == cCandLUAvg) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // left+upper edge avg | |
| else if (cand_index == cCandLUBlendStrong) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // left+upper edge stronger distance blend | |
| else if (cand_index == cCandGradient) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; // gradient | |
| else if (cand_index == cCandGradientDamped) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; // damped gradient | |
| else if (cand_index == cCandDiagAvg) | |
| pCand_log_blk = (pLeft_diag_log_blk && pRight_diag_log_blk) ? pLeft_diag_log_blk : nullptr; // left/right diagonal avg | |
| else if (cand_index == cCandDiagEdgeBlend) | |
| pCand_log_blk = (pLeft_diag_log_blk && pRight_diag_log_blk) ? pLeft_diag_log_blk : nullptr; // diagonal edge blend | |
| else if (cand_index == cCandUpperDiagEdgeBlend) | |
| pCand_log_blk = (pUp_log_blk && pLeft_diag_log_blk && pRight_diag_log_blk) ? pLeft_diag_log_blk : nullptr; // upper + diagonal edge blend | |
| else if ((cand_index == cCandMED) || (cand_index == cCandGAB)) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; // MED / gradient-adaptive blend | |
| else if (cand_index == cCandPlaneFit) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // LS plane fit | |
| else if ((cand_index == cCandDDL) && pUp_log_blk && pRight_diag_log_blk) | |
| pCand_log_blk = pUp_log_blk; | |
| else if (cand_index == cCandDDR) | |
| pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; | |
| if (!pCand_log_blk) | |
| return false; | |
| } | |
| int* pWeight_predictions = nullptr; | |
| int weight_preds[16]; | |
| if (pCand_log_blk) | |
| { | |
| for (uint32_t w = 0; w < 16; w++) | |
| { | |
| weight_preds[w] = fetch_w(pCand_log_blk, p, w); | |
| } | |
| int orig_weight_preds[16]; | |
| memcpy(orig_weight_preds, weight_preds, sizeof(orig_weight_preds)); | |
| if (cand_index == cCandLeftEdge) | |
| { | |
| // left edge | |
| for (uint32_t y = 0; y < 4; y++) | |
| for (uint32_t x = 0; x < 4; x++) | |
| weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(3, y)]; | |
| } | |
| else if (cand_index == cCandUpperEdge) | |
| { | |
| // upper edge | |
| for (uint32_t y = 0; y < 4; y++) | |
| for (uint32_t x = 0; x < 4; x++) | |
| weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(x, 3)]; | |
| } | |
| else if ((cand_index == cCandLUBlend) || (cand_index == cCandLUAvg) || (cand_index == cCandLUBlendStrong)) | |
| { | |
| // left+upper edge blend variants. | |
| // pCand_log_blk is pLeft_log_blk here, so orig_weight_preds contains the left block. | |
| // Pull upper edge directly from pUp_log_blk. | |
| int upper_edge[4]; | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t w = index_from_xy(x, 3); // upper block's bottom edge | |
| upper_edge[x] = fetch_w(pUp_log_blk, p, w); | |
| } | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| const int left_val = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const int upper_val = upper_edge[x]; | |
| int pred; | |
| if (cand_index == cCandLUBlend) | |
| { | |
| // Existing distance-weighted blend. | |
| const int wl = 4 - static_cast<int>(x); // 4,3,2,1 | |
| const int wu = 4 - static_cast<int>(y); // 4,3,2,1 | |
| const int den = wl + wu; | |
| pred = (wl * left_val + wu * upper_val + (den >> 1)) / den; | |
| } | |
| else if (cand_index == cCandLUAvg) | |
| { | |
| // Simple average. | |
| pred = (left_val + upper_val + 1) >> 1; | |
| } | |
| else // cCandLUBlendStrong | |
| { | |
| // Stronger distance weighting: trust the nearest edge more. | |
| const int dx = 4 - static_cast<int>(x); // 4,3,2,1 | |
| const int dy = 4 - static_cast<int>(y); // 4,3,2,1 | |
| const int wl = dx * dx; // 16,9,4,1 | |
| const int wu = dy * dy; // 16,9,4,1 | |
| const int den = wl + wu; | |
| pred = (wl * left_val + wu * upper_val + (den >> 1)) / den; | |
| } | |
| weight_preds[index_from_xy(x, y)] = pred; | |
| } | |
| } | |
| } | |
| else if (cand_index == cCandReflectLeft) | |
| { | |
| // reflect left | |
| for (uint32_t y = 0; y < 4; y++) | |
| for (uint32_t x = 0; x < 4; x++) | |
| weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(3 - x, y)]; | |
| } | |
| else if (cand_index == cCandReflectUpper) | |
| { | |
| // reflect upper | |
| for (uint32_t y = 0; y < 4; y++) | |
| for (uint32_t x = 0; x < 4; x++) | |
| weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(x, 3 - y)]; | |
| } | |
| else if ((cand_index == cCandGradient) || (cand_index == cCandGradientDamped) || (cand_index == cCandMED) || (cand_index == cCandGAB)) | |
| { | |
| int upper_edge[4]; | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t w = index_from_xy(x, 3); // upper block's bottom edge | |
| upper_edge[x] = fetch_w(pUp_log_blk, p, w); | |
| } | |
| const uint32_t corner_w = index_from_xy(3, 3); // upper-left block's bottom-right | |
| int corner_val; | |
| corner_val = fetch_w(pLeft_diag_log_blk, p, corner_w); | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| const int left_val = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const int upper_val = upper_edge[x]; | |
| if (cand_index == cCandGradient) | |
| { | |
| int grad = left_val + upper_val - corner_val; | |
| grad = basisu::clamp<int>(grad, 0, 64); | |
| weight_preds[index_from_xy(x, y)] = grad; | |
| } | |
| else if (cand_index == cCandGradientDamped) | |
| { | |
| int grad = left_val + upper_val - corner_val; | |
| grad = basisu::clamp<int>(grad, 0, 64); | |
| // Damped gradient: blend gradient with the proven #7 predictor. | |
| const int wl = 4 - static_cast<int>(x); | |
| const int wu = 4 - static_cast<int>(y); | |
| const int den = wl + wu; | |
| const int blend7 = (wl * left_val + wu * upper_val + (den >> 1)) / den; | |
| weight_preds[index_from_xy(x, y)] = (grad + blend7 + 1) >> 1; | |
| } | |
| else if (cand_index == cCandMED) | |
| { | |
| // MED (Median Edge Detector, JPEG-LS / LOCO-I). | |
| // Plane predictor wrapped in a per-sample edge switch: | |
| // if the corner is the local extreme, an edge passes | |
| // between the neighbors -- predict from the neighbor on | |
| // the current sample's side instead of extrapolating | |
| // through the edge (which is #12's overshoot failure). | |
| const int mn = basisu::minimum(left_val, upper_val); | |
| const int mx = basisu::maximum(left_val, upper_val); | |
| int pred; | |
| if (corner_val >= mx) | |
| pred = mn; | |
| else if (corner_val <= mn) | |
| pred = mx; | |
| else | |
| pred = left_val + upper_val - corner_val; // in (mn, mx) here, cannot overshoot | |
| weight_preds[index_from_xy(x, y)] = basisu::clamp<int>(pred, 0, 64); | |
| } | |
| else // cCandGAB | |
| { | |
| // GAB: gradient-adaptive blend (CALIC-spirit, 3 samples). | |
| // |left-corner| large => a horizontal edge crossed between | |
| // the corner row and this row, so the left sample is on the | |
| // current sample's side of it: trust it more. Symmetrically | |
| // for |upper-corner| and vertical edges. A self-normalizing | |
| // soft MED whose weights come from the data, not position. | |
| const int wl = basisu::iabs(left_val - corner_val) + 1; | |
| const int wu = basisu::iabs(upper_val - corner_val) + 1; | |
| const int den = wl + wu; | |
| const int pred = (wl * left_val + wu * upper_val + (den >> 1)) / den; | |
| weight_preds[index_from_xy(x, y)] = pred; // convex blend of in-range values: already in [0,64] | |
| } | |
| } | |
| } | |
| } | |
| else if (cand_index == cCandDiagAvg) | |
| { | |
| // Average upper-left and upper-right diagonal blocks. | |
| // pCand_log_blk is pLeft_diag_log_blk here, so orig_weight_preds contains upper-left. | |
| // Pull upper-right directly from pRight_diag_log_blk. | |
| for (uint32_t w = 0; w < 16; w++) | |
| { | |
| int right_diag_val; | |
| right_diag_val = fetch_w(pRight_diag_log_blk, p, w); | |
| weight_preds[w] = (orig_weight_preds[w] + right_diag_val + 1) >> 1; | |
| } | |
| } | |
| else if (cand_index == cCandDiagEdgeBlend) | |
| { | |
| // Blend upper-left block's right edge with upper-right block's left edge. | |
| // pCand_log_blk is pLeft_diag_log_blk, so orig_weight_preds contains upper-left. | |
| // Pull upper-right left edge directly from pRight_diag_log_blk. | |
| // | |
| // For each row y: | |
| // L = upper-left[3,y] | |
| // R = upper-right[0,y] | |
| // Then interpolate across x. | |
| int right_diag_left_edge[4]; | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| const uint32_t w = index_from_xy(0, y); // upper-right block's left edge | |
| right_diag_left_edge[y] = fetch_w(pRight_diag_log_blk, p, w); | |
| } | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| const int left_val = orig_weight_preds[index_from_xy(3, y)]; // upper-left right edge | |
| const int right_val = right_diag_left_edge[y]; // upper-right left edge | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| // x=0 mostly left_val, x=3 mostly right_val. | |
| // Use 4-sample interpolation: 3/0, 2/1, 1/2, 0/3. | |
| const int pred = ((3 - static_cast<int>(x)) * left_val + | |
| static_cast<int>(x) * right_val + 1) / 3; | |
| weight_preds[index_from_xy(x, y)] = pred; | |
| } | |
| } | |
| } | |
| else if (cand_index == cCandUpperDiagEdgeBlend) | |
| { | |
| // Blend upper edge predictor with diagonal edge blend. | |
| // | |
| // upper_edge[x] = upper block's bottom edge | |
| // diag_blend[x,y] = horizontal interpolation between: | |
| // upper-left block's right edge and upper-right block's left edge | |
| // | |
| // This combines direct top continuation with previous-row lateral structure. | |
| int upper_edge[4]; | |
| int right_diag_left_edge[4]; | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t up_w = index_from_xy(x, 3); // upper block's bottom edge | |
| upper_edge[x] = fetch_w(pUp_log_blk, p, up_w); | |
| } | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| const uint32_t rd_w = index_from_xy(0, y); // upper-right block's left edge | |
| right_diag_left_edge[y] = fetch_w(pRight_diag_log_blk, p, rd_w); | |
| } | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| const int left_diag_right_val = orig_weight_preds[index_from_xy(3, y)]; // upper-left right edge | |
| const int right_diag_left_val = right_diag_left_edge[y]; | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| // Same as #15: lateral predictor from upper-left/right diagonal edges. | |
| const int diag_blend = | |
| ((3 - static_cast<int>(x)) * left_diag_right_val + | |
| static_cast<int>(x) * right_diag_left_val + 1) / 3; | |
| // Same as #6: direct upper edge replicated downward. | |
| const int up_val = upper_edge[x]; | |
| // Trust upper edge more near y=0, trust diagonal lateral structure more lower in the block. | |
| const int wu = 4 - static_cast<int>(y); // 4,3,2,1 | |
| const int wd = 1 + static_cast<int>(y); // 1,2,3,4 | |
| const int den = wu + wd; // always 5 | |
| weight_preds[index_from_xy(x, y)] = | |
| (wu * up_val + wd * diag_blend + (den >> 1)) / den; | |
| } | |
| } | |
| } | |
| else if (cand_index == cCandPlaneFit) | |
| { | |
| // LS plane fit through left + upper edges. | |
| // pCand_log_blk is pLeft_log_blk, so orig_weight_preds holds the left block. | |
| int upper_edge[4]; | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t w = index_from_xy(x, 3); // upper block's bottom edge | |
| upper_edge[x] = fetch_w(pUp_log_blk, p, w); | |
| } | |
| int left_edge[4]; | |
| for (uint32_t y = 0; y < 4; y++) | |
| left_edge[y] = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge | |
| const int sum_u = upper_edge[0] + upper_edge[1] + upper_edge[2] + upper_edge[3]; | |
| const int sum_l = left_edge[0] + left_edge[1] + left_edge[2] + left_edge[3]; | |
| // LS slopes * 10 | |
| const int gx10 = -3 * upper_edge[0] - upper_edge[1] + upper_edge[2] + 3 * upper_edge[3]; | |
| const int gy10 = -3 * left_edge[0] - left_edge[1] + left_edge[2] + 3 * left_edge[3]; | |
| const int base = 5 * (sum_u + sum_l); // mean*40, anchored at edge centroid (.25,.25) | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const int num = base + gx10 * (4 * (int)x - 1) + gy10 * (4 * (int)y - 1); | |
| // round-half-up = floor((num + 20) / 40); numerator can be negative | |
| // (negative slopes), and C++ '/' truncates toward zero, so force floor. | |
| const int t = num + 20; | |
| const int pred_unclamped = (t >= 0) ? (t / 40) : -((-t + 39) / 40); | |
| weight_preds[index_from_xy(x, y)] = basisu::clamp<int>(pred_unclamped, 0, 64); | |
| } | |
| } | |
| } | |
| else if (cand_index == cCandDDL) | |
| { | |
| // Diagonal-down-left (H.264 4x4 intra mode 3 analog): propagate the | |
| // extended top row (upper block's bottom edge + upper-RIGHT block's | |
| // bottom edge) down-left at 45 degrees, with 1-2-1 smoothing along the | |
| // diagonal. Captures 45-degree edges/stripes entering from the top-right | |
| // quadrant -- a direction nothing else in the bank can represent. | |
| // pCand_log_blk is pUp_log_blk; requires pRight_diag_log_blk. | |
| int T[8]; | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t w = index_from_xy(x, 3); | |
| T[x] = fetch_w(pUp_log_blk, p, w); | |
| T[4 + x] = fetch_w(pRight_diag_log_blk, p, w); | |
| } | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t d = x + y; // 0..6 | |
| int pred; | |
| if (d == 6) | |
| pred = (T[6] + 3 * T[7] + 2) >> 2; | |
| else | |
| pred = (T[d] + 2 * T[d + 1] + T[d + 2] + 2) >> 2; | |
| weight_preds[index_from_xy(x, y)] = pred; // smoothed avg of [0,64] values: in range | |
| } | |
| } | |
| } | |
| else if (cand_index == cCandDDR) | |
| { | |
| // Diagonal-down-right (H.264 4x4 intra mode 4 analog): propagate at 45 | |
| // degrees from the top-left, sourcing the left column, the corner, and | |
| // the top row, 1-2-1 smoothed. Captures 45-degree structure entering | |
| // from the top-left quadrant. | |
| // pCand_log_blk is pLeft_log_blk; requires pUp_log_blk and pLeft_diag_log_blk. | |
| // A[0..8]: A[0..3] = left column bottom-to-top, A[4] = corner, A[5..8] = top row. | |
| int A[9]; | |
| for (uint32_t y = 0; y < 4; y++) | |
| A[3 - y] = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge, reversed | |
| { | |
| const uint32_t cw = index_from_xy(3, 3); | |
| A[4] = fetch_w(pLeft_diag_log_blk, p, cw); | |
| } | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const uint32_t w = index_from_xy(x, 3); | |
| A[5 + x] = fetch_w(pUp_log_blk, p, w); | |
| } | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| const int d = 4 + (int)x - (int)y; // 1..7; texels on diagonal k share A-index k | |
| const int pred = (A[d - 1] + 2 * A[d] + A[d + 1] + 2) >> 2; | |
| weight_preds[index_from_xy(x, y)] = pred; | |
| } | |
| } | |
| } | |
| pWeight_predictions = weight_preds; | |
| } | |
| if ((amp_code) && (pWeight_predictions)) | |
| { | |
| // Amplitude code: generalization of the old inversion flag. | |
| // All transforms are about the prediction's own mean (per | |
| // plane), so they negate/scale the ACs while leaving the DC | |
| // for the DC coefficient -- the old 64-w flip also | |
| // complemented the DC, forcing a DC correction symbol | |
| // whenever the content mean wasn't complementary. | |
| int sum = 0; | |
| for (uint32_t i = 0; i < 16; i++) | |
| sum += pWeight_predictions[i]; | |
| const int mean = (sum + 8) >> 4; | |
| for (uint32_t i = 0; i < 16; i++) | |
| { | |
| const int w = pWeight_predictions[i]; | |
| int v; | |
| if (amp_code == 1) | |
| { | |
| // flip about mean (s = -1): pure AC negation | |
| v = basisu::clamp<int>(2 * mean - w, 0, 64); | |
| //v = 64 - w; | |
| } | |
| else if (amp_code == 2) | |
| { | |
| // half contrast (s = +1/2): mean + (w - mean)/2, half-up | |
| v = (w + mean + 1) >> 1; // closed in [0,64], no clamp needed | |
| } | |
| else // amp_code == 3 | |
| { | |
| // half contrast of the flip (s = -1/2): compose 1 then 2 | |
| const int f = basisu::clamp<int>(2 * mean - w, 0, 64); | |
| v = (f + mean + 1) >> 1; | |
| } | |
| pWeight_predictions[i] = v; | |
| } | |
| } | |
| if (!pWeight_predictions) | |
| return false; | |
| memcpy(pOut_preds, pWeight_predictions, 16 * sizeof(int)); | |
| return true; | |
| } | |
| static inline int unpack_coeff_b(uint32_t v) | |
| { | |
| assert(v <= 255); | |
| return (int)v; | |
| } | |
| // LSB-first bit reader matching basisu::bitwise_coder's packing exactly | |
| // (first bit written lands in bit 0 of byte 0; values may cross byte | |
| // boundaries). Total: reads past the end return false. | |
| struct lsb_bit_reader | |
| { | |
| const uint8_t* m_p = nullptr; | |
| uint64_t m_total_bits = 0; | |
| uint64_t m_bit_ofs = 0; | |
| void init(const uint8_t* p, size_t size_bytes) { m_p = p; m_total_bits = (uint64_t)size_bytes * 8; m_bit_ofs = 0; } | |
| // seek to a bit range [start_bit, end_bit) within the blob -- used for | |
| // per-stripe decoding (the stripe's bits occupy exactly this range) | |
| void init_range(const uint8_t* p, uint64_t start_bit, uint64_t end_bit) { m_p = p; m_bit_ofs = start_bit; m_total_bits = end_bit; } | |
| bool get_bits(uint32_t n, uint32_t& v) | |
| { | |
| assert(n <= 32); | |
| v = 0; | |
| if (n > (m_total_bits - m_bit_ofs)) | |
| return false; | |
| if (!n) | |
| return true; | |
| const uint64_t byte_idx = m_bit_ofs >> 3; | |
| const uint32_t bit_idx = (uint32_t)(m_bit_ofs & 7); | |
| const uint32_t num_bytes = (bit_idx + n + 7) >> 3; // 1..5 | |
| uint64_t bits = 0; | |
| for (uint32_t i = 0; i < num_bytes; i++) | |
| bits |= ((uint64_t)m_p[byte_idx + i]) << (i * 8); | |
| v = (uint32_t)((bits >> bit_idx) & ((1ull << n) - 1ull)); | |
| m_bit_ofs += n; | |
| return true; | |
| } | |
| // at most 7 zero pad bits may remain | |
| bool is_fully_consumed() const { return (m_total_bits - m_bit_ofs) < 8; } | |
| }; | |
| // forward byte cursor over one blob; total | |
| struct byte_cursor | |
| { | |
| const uint8_t* m_p = nullptr; | |
| uint32_t m_size = 0, m_ofs = 0; | |
| void init(const blob_stream_reader& rdr, uint32_t id) { m_p = rdr.get_ptr(id); m_size = rdr.get_size(id); m_ofs = 0; } | |
| // seek to a byte range [start, end) within the blob -- per-stripe decode | |
| void init_range(const blob_stream_reader& rdr, uint32_t id, uint32_t start, uint32_t end) { m_p = rdr.get_ptr(id); m_ofs = start; m_size = end; } | |
| bool get(uint8_t& b) { if (m_ofs >= m_size) return false; b = m_p[m_ofs++]; return true; } | |
| bool is_fully_consumed() const { return m_ofs == m_size; } | |
| }; | |
| bool blob_stream_reader::init_internal(const void* pData, size_t data_size, uint64_t max_total_uncomp) | |
| { | |
| clear(); | |
| const uint8_t* pBytes = static_cast<const uint8_t*>(pData); | |
| if ((!pBytes) || (data_size < 3)) // magic + count + magic minimum | |
| return false; | |
| uint64_t ofs = 0; | |
| if (pBytes[ofs++] != BLOB_STREAM_MAGIC_BEGIN) | |
| return false; | |
| const uint32_t num_blobs = pBytes[ofs++]; | |
| // pass 1: walk + validate the directory, total the arena size | |
| struct entry { uint32_t id, uncomp_size, stored_size; uint64_t data_ofs; }; | |
| entry entries[255]; | |
| uint64_t total_arena = 0; | |
| for (uint32_t i = 0; i < num_blobs; i++) | |
| { | |
| if (ofs >= data_size) | |
| return false; | |
| entry& e = entries[i]; | |
| const uint8_t id_flag = pBytes[ofs++]; | |
| e.id = id_flag & 0x7Fu; | |
| const bool compressed = (id_flag & 0x80u) != 0; | |
| if (!read_varint(pBytes, data_size, ofs, e.uncomp_size)) | |
| return false; | |
| e.stored_size = 0; | |
| if (compressed) | |
| { | |
| if (!read_varint(pBytes, data_size, ofs, e.stored_size)) | |
| return false; | |
| if (!e.stored_size) | |
| return false; | |
| } | |
| e.data_ofs = ofs; | |
| if (!e.uncomp_size) | |
| return false; // empty blobs are never stored | |
| if (m_sizes[e.id] || m_ptrs[e.id]) | |
| return false; // duplicate id | |
| const uint64_t stored_bytes = e.stored_size ? e.stored_size : e.uncomp_size; | |
| if ((ofs + stored_bytes) > data_size) | |
| return false; | |
| ofs += stored_bytes; | |
| if (e.stored_size) | |
| { | |
| if (e.stored_size >= e.uncomp_size) | |
| return false; // compressed must be strictly smaller (writer guarantees it) | |
| total_arena += e.uncomp_size; | |
| if (total_arena > max_total_uncomp) | |
| return false; | |
| } | |
| // mark id as seen (real ptr/size set in pass 2) | |
| m_sizes[e.id] = e.uncomp_size; | |
| m_ptrs[e.id] = pBytes; // placeholder, nonzero for dup detection | |
| } | |
| // end marker must be the exact final byte: rejects truncation, | |
| // trailing garbage, and directory/data length disagreements | |
| if ((ofs != (data_size - 1)) || (pBytes[ofs] != BLOB_STREAM_MAGIC_END)) | |
| return false; | |
| // pass 2: single arena allocation, decompress, wire up pointers | |
| if (!m_arena.try_resize((size_t)total_arena)) | |
| return false; | |
| uint64_t arena_ofs = 0; | |
| for (uint32_t i = 0; i < num_blobs; i++) | |
| { | |
| const entry& e = entries[i]; | |
| if (!e.stored_size) | |
| { | |
| m_ptrs[e.id] = pBytes + e.data_ofs; // raw: zero copy into input | |
| } | |
| else | |
| { | |
| uint8_t* pDst = m_arena.data() + arena_ofs; | |
| #if BASISD_SUPPORT_KTX2_ZSTD | |
| const size_t res = ZSTD_decompress(pDst, e.uncomp_size, pBytes + e.data_ofs, e.stored_size); | |
| if (ZSTD_isError(res) || (res != e.uncomp_size)) | |
| return false; | |
| #else | |
| BASISU_NOTE_UNUSED(pDst); | |
| return false; // zstd disabled at compile time | |
| #endif | |
| m_ptrs[e.id] = pDst; | |
| arena_ofs += e.uncomp_size; | |
| } | |
| } | |
| return true; | |
| } | |
| bool image_unpacker::init( | |
| const byte_span& comp, | |
| decode_init_callback_ptr pInit_callback, void* pInit_callback_data, | |
| decode_block_callback_ptr pBlock_callback, void* pBlock_callback_data) | |
| { | |
| m_initialized = false; // set true ONLY on full success below; every failure path leaves it false | |
| m_tiny_mip = false; | |
| m_block_cb = pBlock_callback; | |
| m_block_data = pBlock_callback_data; | |
| #if !BASISD_SUPPORT_KTX2_ZSTD | |
| BASISU_DEVEL_ERROR("xbc7::unpack_image: XBC7 decoding requires zstd (BASISD_SUPPORT_KTX2_ZSTD is 0)"); | |
| return false; | |
| #endif | |
| // ---- format dispatch on the leading byte ---- | |
| // 0xB7 = blob container (blob_stream_reader validates this marker itself). | |
| // 0xB8/0xB9 = tiny-mip ([marker][num_blocks_x:u8][num_blocks_y:u8] then 16 | |
| // bytes per packed BC7 block); 0xB8 = no alpha, 0xB9 = has alpha | |
| // (the has_alpha bit rides in the marker). Anything else: not XBC7. | |
| if (comp.size() < 1) | |
| return false; | |
| #ifndef NDEBUG | |
| // Debug builds: report the input buffer size and the stream type. | |
| { | |
| const char* pType = "regular (blob)"; | |
| if (comp[0] == 0xB8) | |
| pType = "tiny-mip (no alpha)"; | |
| else if (comp[0] == 0xB9) | |
| pType = "tiny-mip (alpha)"; | |
| else if (comp[0] != BLOB_STREAM_MAGIC_BEGIN) | |
| pType = "unknown / invalid marker"; | |
| fmt_debug_printf("XBC7 decode: input {} bytes, type: {}\n", (uint64_t)comp.size(), pType); | |
| } | |
| #endif | |
| if ((comp[0] == 0xB8) || (comp[0] == 0xB9)) | |
| return init_tiny_mip(comp, comp[0] == 0xB9, pInit_callback, pInit_callback_data); | |
| if (comp[0] != BLOB_STREAM_MAGIC_BEGIN) | |
| return false; // unknown format marker | |
| if (!m_rdr.init(comp.data(), comp.size())) | |
| return false; | |
| // ---- header ---- | |
| if (m_rdr.get_size(cBlobHeader) != sizeof(xbc7_header)) | |
| return false; | |
| xbc7_header hdr; | |
| memcpy((void *)&hdr, m_rdr.get_ptr(cBlobHeader), sizeof(hdr)); | |
| const uint32_t width = hdr.m_width_in_texels; | |
| const uint32_t height = hdr.m_height_in_texels; | |
| m_global_q = hdr.m_dct_q; | |
| if ((!width) || (!height)) | |
| return false; | |
| const uint32_t XBC7_MAX_SUPPORTED_DIM = 16384; | |
| if ((width > XBC7_MAX_SUPPORTED_DIM) || (height > XBC7_MAX_SUPPORTED_DIM)) | |
| return false; | |
| if ((m_global_q < 1) || (m_global_q > 100)) | |
| return false; | |
| if (hdr.m_flags & ~XBC7_FLAG_HAS_ALPHA) | |
| return false; // unknown flags | |
| m_width = width; | |
| m_height = height; | |
| m_has_alpha = (hdr.m_flags & XBC7_FLAG_HAS_ALPHA) != 0; | |
| m_num_blocks_x = (width + 3) / 4; | |
| m_num_blocks_y = (height + 3) / 4; | |
| const uint32_t total_blocks = m_num_blocks_x * m_num_blocks_y; // <= 16384^2, no overflow | |
| // ---- stripe geometry (also governs the implicit solid-block clamp) ---- | |
| m_num_stripes = hdr.m_num_stripes; | |
| if ((!m_num_stripes) || (m_num_stripes > m_num_blocks_y)) | |
| return false; | |
| if (m_num_stripes > XBC7_MAX_ENCODER_STRIPES) | |
| return false; | |
| compute_stripe_ranges(m_num_blocks_y, m_num_stripes, m_stripes); | |
| // ---- paranoia gate: structural size check BEFORE any allocation ---- | |
| // One command byte per block makes this a necessary condition and | |
| // transitively bounds every allocation, killing the huge-dims DoS. | |
| if (m_rdr.get_size(cBlobCommands) != total_blocks) | |
| return false; | |
| // ---- per-stripe seek table ---- | |
| // m_seek(id, s) = start offset of stripe s in blob id (a BYTE offset, | |
| // or a BIT offset for the three bit blobs); row m_num_stripes holds the | |
| // end sentinel (blob byte size, or total bits). Stripe 0 starts at 0. | |
| // Single-stripe files carry no table -- the whole blob is stripe 0. | |
| const uint32_t SEEK_NUM_STREAMS = (uint32_t)cBlobStripeSeekTable - 1; // ids 1..25 | |
| m_seek.resize((uint32_t)cBlobStripeSeekTable, m_num_stripes + 1); // (id) x (stripe) | |
| m_seek.set_all(0); | |
| for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++) | |
| { | |
| const bool bit_blob = (id == cBlobCoeffSigns) || (id == cBlobPBits) || (id == cBlobEPRaw); | |
| m_seek(id, m_num_stripes) = bit_blob ? ((uint64_t)m_rdr.get_size(id) * 8) : (uint64_t)m_rdr.get_size(id); | |
| } | |
| if (m_num_stripes > 1) | |
| { | |
| if (m_rdr.get_size(cBlobStripeSeekTable) != m_num_stripes * SEEK_NUM_STREAMS * 4) | |
| return false; | |
| // Entries are DELTAS from the previous stripe's start, stripe-major, | |
| // stored byte-plane (SoA) transposed: byte b of entry e lives at | |
| // pT[b * num_entries + e] (the encoder's inverse of this layout). | |
| // Reconstruct absolute offsets with a running prefix sum, validating | |
| // as we go (all BEFORE any block decode): stripe 0's delta must be 0, | |
| // and each running offset must stay within [0, blob_end] (blob_end = | |
| // byte size, or total bits for the three bit blobs). Monotonicity is | |
| // automatic -- deltas are unsigned, so the running sum never | |
| // decreases. The range test rejects a corrupted table immediately; a | |
| // wild byte/bit offset can never reach the cursor seek in decode_stripe. | |
| const uint8_t* pT = m_rdr.get_ptr(cBlobStripeSeekTable); | |
| const uint32_t num_entries = m_num_stripes * SEEK_NUM_STREAMS; | |
| for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++) | |
| { | |
| const uint64_t blob_end = m_seek(id, m_num_stripes); // sentinel set above | |
| uint64_t running = 0; | |
| for (uint32_t st = 0; st < m_num_stripes; st++) | |
| { | |
| const uint32_t e = st * SEEK_NUM_STREAMS + (id - 1); | |
| const uint64_t delta = (uint64_t)pT[e] | ((uint64_t)pT[num_entries + e] << 8) | | |
| ((uint64_t)pT[2u * num_entries + e] << 16) | ((uint64_t)pT[3u * num_entries + e] << 24); | |
| if ((st == 0) && (delta != 0)) | |
| return false; // stripe 0 must start at offset 0 | |
| running += delta; // unsigned add: offsets are monotonic by construction | |
| if (running > blob_end) | |
| return false; // offset past end of blob | |
| m_seek(id, st) = running; | |
| } | |
| } | |
| } | |
| // ---- decode state + callbacks ---- | |
| // The decoder always maintains its own full-image logical-block buffer: | |
| // causal prediction (endpoint DPCM / repeat / weight predictors) reads | |
| // already-decoded neighbors. Finished blocks are streamed to the caller | |
| // via the block callback; the caller owns any persistent storage/packing. | |
| m_log_blks.resize(m_num_blocks_x, m_num_blocks_y); | |
| // Hand the caller the header geometry/metadata exactly once, before any | |
| // block, so it can validate and allocate. A false return aborts. | |
| if (pInit_callback) | |
| { | |
| if (!pInit_callback(m_num_blocks_x, m_num_blocks_y, m_width, m_height, m_global_q, m_has_alpha, pInit_callback_data)) | |
| return false; | |
| } | |
| m_initialized = true; | |
| return true; | |
| } | |
| bool image_unpacker::init_tiny_mip( | |
| const byte_span& comp, bool has_alpha, | |
| decode_init_callback_ptr pInit_callback, void* pInit_callback_data) | |
| { | |
| if (comp.size() < 3) | |
| return false; | |
| const uint32_t nbx = comp[1]; | |
| const uint32_t nby = comp[2]; | |
| if ((!nbx) || (!nby)) | |
| return false; | |
| // exact-size tripwire: the 3-byte header plus exactly the BC7 blocks | |
| const uint64_t expected = 3ull + (uint64_t)nbx * (uint64_t)nby * 16ull; | |
| if ((uint64_t)comp.size() != expected) | |
| return false; | |
| m_tiny_mip = true; | |
| // checked region pointer to all the BC7 blocks (validated once); aliases | |
| // the span's underlying buffer, which must outlive the decoder | |
| m_tiny_blocks = comp.checked_ptr(3, (size_t)nbx * (size_t)nby * 16); // -> first block | |
| m_num_blocks_x = nbx; | |
| m_num_blocks_y = nby; | |
| m_num_stripes = 1; | |
| m_width = nbx * 4; // block-aligned; tiny-mip stores no exact texel dims | |
| m_height = nby * 4; | |
| m_global_q = 0; // no DCT in tiny-mip | |
| m_has_alpha = has_alpha; // from the stream marker (0xB8 = no alpha, 0xB9 = alpha) | |
| if (pInit_callback) | |
| { | |
| if (!pInit_callback(m_num_blocks_x, m_num_blocks_y, m_width, m_height, m_global_q, m_has_alpha, pInit_callback_data)) | |
| return false; | |
| } | |
| m_initialized = true; | |
| return true; | |
| } | |
| bool image_unpacker::decode_stripe(uint32_t stripe_index) | |
| { | |
| // must follow a successful init() -- guard in ALL builds (assert traps | |
| // the bug in debug; the return false keeps release total) | |
| assert(m_initialized && "decode_stripe() called without a successful init()"); | |
| if (!m_initialized) | |
| return false; | |
| assert(stripe_index < m_num_stripes); | |
| if (stripe_index >= m_num_stripes) | |
| return false; | |
| // tiny-mip is a single conceptual stripe (m_num_stripes == 1): decode all | |
| // its blocks straight from the raw BC7 data, no prediction/blobs involved. | |
| if (m_tiny_mip) | |
| return decode_tiny_mip(); | |
| const uint32_t first_row = m_stripes[stripe_index].m_first_block_row; | |
| const uint32_t end_row = first_row + m_stripes[stripe_index].m_num_block_rows; | |
| // every reference is clamped to this stripe AABB (mirrors the encoder) | |
| const tile_bounds stripe_tile = { 0, (int)first_row, (int)m_num_blocks_x - 1, (int)end_row - 1 }; | |
| // aliases so the per-block body below resolves to member state | |
| blob_stream_reader& rdr = m_rdr; | |
| const uint32_t num_blocks_x = m_num_blocks_x; | |
| const uint32_t global_q = m_global_q; | |
| const bool has_alpha = m_has_alpha; | |
| vector2D<basist::bc7u::log_bc7_block>& log_blks = m_log_blks; | |
| // cursors seeked to this stripe's range [seek(id,s), seek(id,s+1)) | |
| const uint32_t s = stripe_index; | |
| byte_cursor commands; commands.init_range(rdr, cBlobCommands, (uint32_t)m_seek(cBlobCommands, s), (uint32_t)m_seek(cBlobCommands, s + 1)); | |
| byte_cursor configs; configs.init_range(rdr, cBlobBC7BlockConfig, (uint32_t)m_seek(cBlobBC7BlockConfig, s), (uint32_t)m_seek(cBlobBC7BlockConfig, s + 1)); | |
| byte_cursor partitions2; partitions2.init_range(rdr, cBlobPartition2, (uint32_t)m_seek(cBlobPartition2, s), (uint32_t)m_seek(cBlobPartition2, s + 1)); | |
| byte_cursor partitions3; partitions3.init_range(rdr, cBlobPartition3, (uint32_t)m_seek(cBlobPartition3, s), (uint32_t)m_seek(cBlobPartition3, s + 1)); | |
| byte_cursor predictors; predictors.init_range(rdr, cBlobWeightPredictors, (uint32_t)m_seek(cBlobWeightPredictors, s), (uint32_t)m_seek(cBlobWeightPredictors, s + 1)); | |
| byte_cursor dc_coeffs; dc_coeffs.init_range(rdr, cBlobDCCoeffsSmall, (uint32_t)m_seek(cBlobDCCoeffsSmall, s), (uint32_t)m_seek(cBlobDCCoeffsSmall, s + 1)); // encoder writes ALL DC here | |
| byte_cursor ac_coeffs; ac_coeffs.init_range(rdr, cBlobACCoeffs, (uint32_t)m_seek(cBlobACCoeffs, s), (uint32_t)m_seek(cBlobACCoeffs, s + 1)); | |
| byte_cursor solid_deltas; solid_deltas.init_range(rdr, cBlobSolidRGBADeltas, (uint32_t)m_seek(cBlobSolidRGBADeltas, s), (uint32_t)m_seek(cBlobSolidRGBADeltas, s + 1)); | |
| byte_cursor ep_block_index; ep_block_index.init_range(rdr, cBlobEPBlockIndex, (uint32_t)m_seek(cBlobEPBlockIndex, s), (uint32_t)m_seek(cBlobEPBlockIndex, s + 1)); | |
| byte_cursor raw_weights; raw_weights.init_range(rdr, cBlobRawWeightBits, (uint32_t)m_seek(cBlobRawWeightBits, s), (uint32_t)m_seek(cBlobRawWeightBits, s + 1)); | |
| byte_cursor wt_resid[3]; // DPCM weight residuals by bit width: [0]=2, [1]=3, [2]=4 | |
| wt_resid[0].init_range(rdr, cBlobDPCMWeightResid2, (uint32_t)m_seek(cBlobDPCMWeightResid2, s), (uint32_t)m_seek(cBlobDPCMWeightResid2, s + 1)); | |
| wt_resid[1].init_range(rdr, cBlobDPCMWeightResid3, (uint32_t)m_seek(cBlobDPCMWeightResid3, s), (uint32_t)m_seek(cBlobDPCMWeightResid3, s + 1)); | |
| wt_resid[2].init_range(rdr, cBlobDPCMWeightResid4, (uint32_t)m_seek(cBlobDPCMWeightResid4, s), (uint32_t)m_seek(cBlobDPCMWeightResid4, s + 1)); | |
| byte_cursor ep_deltas[8]; // [0..3] = Fine R,G,B,A; [4..7] = Coarse R,G,B,A | |
| for (uint32_t i = 0; i < 4; i++) | |
| { | |
| ep_deltas[i].init_range(rdr, cBlobEPDeltaFineR + i, (uint32_t)m_seek(cBlobEPDeltaFineR + i, s), (uint32_t)m_seek(cBlobEPDeltaFineR + i, s + 1)); | |
| ep_deltas[4 + i].init_range(rdr, cBlobEPDeltaCoarseR + i, (uint32_t)m_seek(cBlobEPDeltaCoarseR + i, s), (uint32_t)m_seek(cBlobEPDeltaCoarseR + i, s + 1)); | |
| } | |
| lsb_bit_reader coeff_signs; coeff_signs.init_range(rdr.get_ptr(cBlobCoeffSigns), m_seek(cBlobCoeffSigns, s), m_seek(cBlobCoeffSigns, s + 1)); | |
| lsb_bit_reader pbits; pbits.init_range(rdr.get_ptr(cBlobPBits), m_seek(cBlobPBits, s), m_seek(cBlobPBits, s + 1)); | |
| lsb_bit_reader ep_raw; ep_raw.init_range(rdr.get_ptr(cBlobEPRaw), m_seek(cBlobEPRaw, s), m_seek(cBlobEPRaw, s + 1)); | |
| xbc7::xbc7_weight_grid_dct_fixed weight_grid_dct_fixed; | |
| weight_grid_dct_fixed.init(); | |
| xbc7::fxvec dct_work_fixed; | |
| for (uint32_t by = first_row; by < end_row; by++) | |
| { | |
| for (uint32_t bx = 0; bx < num_blocks_x; bx++) | |
| { | |
| const basist::bc7u::log_bc7_block* pLeft_log_blk = stripe_tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr; | |
| const basist::bc7u::log_bc7_block* pUp_log_blk = stripe_tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr; | |
| const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = stripe_tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr; | |
| const basist::bc7u::log_bc7_block* pRight_diag_log_blk = stripe_tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr; | |
| basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); | |
| uint8_t cmd_byte; | |
| if (!commands.get(cmd_byte)) | |
| return false; | |
| const uint32_t cmd = cmd_byte & 7; | |
| const uint32_t ep_mode = (cmd_byte >> XBC7_COMMAND_ENDPOINT_MODE_SHIFT) & 7; | |
| const uint32_t wt_mode = (cmd_byte >> XBC7_COMMAND_WEIGHT_MODE_SHIFT) & 1; | |
| if (cmd_byte & 0x80) | |
| return false; // P-frame flag: reserved in v0 | |
| // ---- simple commands: the whole byte is the command id ---- | |
| if (cmd <= (uint32_t)xbc7_command_id::cCmdSolidDPCM) | |
| { | |
| if (cmd_byte != cmd) | |
| return false; // canonical: simple commands carry no EP/WT bits | |
| if (cmd == (uint32_t)xbc7_command_id::cCmdRepeatLast) | |
| { | |
| if (!pLeft_log_blk) | |
| return false; | |
| log_blk = *pLeft_log_blk; | |
| } | |
| else if (cmd == (uint32_t)xbc7_command_id::cCmdRepeatUpper) | |
| { | |
| if (!pUp_log_blk) | |
| return false; | |
| log_blk = *pUp_log_blk; | |
| } | |
| else // cCmdSolidDPCM | |
| { | |
| // neighbor edge-average prediction in decoded-pixel space; | |
| // mirrors the encoder exactly -- INCLUDING its stripe | |
| // seam clamp, since this prediction is implicit (the | |
| // encoder cannot read the stripe above, so neither may we) | |
| int preds[4] = { 0, 0, 0, 0 }; | |
| int num_preds = 0; | |
| if (pLeft_log_blk) | |
| { | |
| for (uint32_t y = 0; y < 4; y++) | |
| { | |
| basist::color_rgba px; | |
| if (!basist::bc7u::unpack_bc7_texel(*pLeft_log_blk, px, 3, y)) | |
| return false; | |
| preds[0] += px.r; preds[1] += px.g; preds[2] += px.b; preds[3] += px.a; | |
| } | |
| num_preds += 4; | |
| } | |
| if (pUp_log_blk) // pUp is already null above the stripe's first row | |
| { | |
| for (uint32_t x = 0; x < 4; x++) | |
| { | |
| basist::color_rgba px; | |
| if (!basist::bc7u::unpack_bc7_texel(*pUp_log_blk, px, x, 3)) | |
| return false; | |
| preds[0] += px.r; preds[1] += px.g; preds[2] += px.b; preds[3] += px.a; | |
| } | |
| num_preds += 4; | |
| } | |
| if (num_preds) | |
| { | |
| for (uint32_t c = 0; c < 4; c++) | |
| preds[c] = (preds[c] + (num_preds / 2)) / num_preds; | |
| } | |
| basist::color_rgba solid_color; | |
| for (uint32_t c = 0; c < (has_alpha ? 4u : 3u); c++) | |
| { | |
| uint8_t delta; | |
| if (!solid_deltas.get(delta)) | |
| return false; | |
| solid_color[c] = (uint8_t)(delta + preds[c]); | |
| } | |
| if (!has_alpha) | |
| solid_color.a = 255; | |
| basist::bc7u::create_solid_blk(log_blk, solid_color); | |
| } | |
| } | |
| else | |
| { | |
| // ---- config ---- | |
| if (cmd == (uint32_t)xbc7_command_id::cCmdNewConfig) | |
| { | |
| uint8_t config_byte; | |
| if (!configs.get(config_byte)) | |
| return false; | |
| if (config_byte & 0xC0) | |
| return false; // reserved bits | |
| const uint32_t mode = config_byte & 7; | |
| const uint32_t rot = (config_byte >> 3) & 3; | |
| const uint32_t sel = (config_byte >> 5) & 1; | |
| basist::bc7u::init_log_blk(log_blk, mode); | |
| if (log_blk.m_num_planes == 2) | |
| log_blk.m_dp_rotation_index = (uint8_t)rot; | |
| else if (rot) | |
| return false; // rotation on a non-dual-plane mode | |
| if (mode == 4) | |
| log_blk.m_mode4_index_selector = (uint8_t)sel; | |
| else if (sel) | |
| return false; // selector outside mode 4 | |
| } | |
| else // reuse config from a neighbor | |
| { | |
| const basist::bc7u::log_bc7_block* pSrc = nullptr; | |
| if (cmd == (uint32_t)xbc7_command_id::cCmdReuseConfigLeft) | |
| pSrc = pLeft_log_blk; | |
| else if (cmd == (uint32_t)xbc7_command_id::cCmdReuseConfigUpper) | |
| pSrc = pUp_log_blk; | |
| else if (cmd == (uint32_t)xbc7_command_id::cCmdReuseConfigLeftDiagonal) | |
| pSrc = pLeft_diag_log_blk; | |
| else // cCmdReuseConfigRightDiagonal | |
| pSrc = pRight_diag_log_blk; | |
| if (!pSrc) | |
| return false; | |
| basist::bc7u::init_log_blk(log_blk, pSrc->m_mode); | |
| log_blk.m_dp_rotation_index = pSrc->m_dp_rotation_index; | |
| log_blk.m_mode4_index_selector = pSrc->m_mode4_index_selector; | |
| } | |
| // ---- partition index (always sent for partitioned modes) ---- | |
| if (log_blk.m_num_partitions == 2) | |
| { | |
| uint8_t pat; | |
| if (!partitions2.get(pat)) | |
| return false; | |
| if (pat >= 64) | |
| return false; | |
| log_blk.m_pattern_index = pat; | |
| } | |
| else if (log_blk.m_num_partitions == 3) | |
| { | |
| uint8_t pat; | |
| if (!partitions3.get(pat)) | |
| return false; | |
| if (pat >= (1u << log_blk.m_pattern_bits)) | |
| return false; // mode 0: index < 16 | |
| log_blk.m_pattern_index = pat; | |
| } | |
| const basist::bc7u::endpoint_format& fmt = basist::bc7u::g_endpoint_formats[log_blk.m_mode]; | |
| const uint32_t num_comps = log_blk.get_num_comps(); | |
| // ---- endpoints ---- | |
| if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw) | |
| { | |
| for (uint32_t subset = 0; subset < log_blk.m_num_partitions; subset++) | |
| { | |
| for (uint32_t c = 0; c < num_comps; c++) | |
| { | |
| for (uint32_t e = 0; e < 2; e++) | |
| { | |
| uint32_t v; | |
| if (!ep_raw.get_bits(log_blk.m_endpoint_bits[c == 3], v)) | |
| return false; | |
| log_blk.m_endpoints[subset][e][c] = (uint8_t)v; | |
| } | |
| } | |
| } | |
| for (uint32_t pb = 0; pb < log_blk.m_num_pbits; pb++) | |
| { | |
| uint32_t v; | |
| if (!ep_raw.get_bits(1, v)) | |
| return false; | |
| if (pb < 6) // to shut up gcc bogus warning | |
| log_blk.m_pbits[pb] = (uint8_t)v; | |
| } | |
| } | |
| else | |
| { | |
| // resolve the endpoint predictor block (mirrors the encoder | |
| // sweep); ep_mode is 3 bits, and 1..7 are all DPCM modes | |
| const basist::bc7u::log_bc7_block* pEP_pred_blk = nullptr; | |
| uint32_t ep_pred_subset = 0; | |
| if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMLeft) | |
| pEP_pred_blk = pLeft_log_blk; | |
| else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMUp) | |
| pEP_pred_blk = pUp_log_blk; | |
| else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftDiagonal) | |
| pEP_pred_blk = pLeft_diag_log_blk; | |
| else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMRightDiagonal) | |
| pEP_pred_blk = pRight_diag_log_blk; | |
| else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftSubset1) | |
| { | |
| pEP_pred_blk = pLeft_log_blk; | |
| ep_pred_subset = 1; | |
| } | |
| else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMUpSubset1) | |
| { | |
| pEP_pred_blk = pUp_log_blk; | |
| ep_pred_subset = 1; | |
| } | |
| else // cCmdEndpointDPCMBlockIndex | |
| { | |
| uint8_t delta_index; | |
| if (!ep_block_index.get(delta_index)) | |
| return false; | |
| if (delta_index >= NUM_XY_DELTAS) | |
| return false; // top 3 bits reserved-zero | |
| const xbc7_xy_delta& delta = g_xbc7_xy_deltas[delta_index]; | |
| const int nx = (int)bx + delta.m_dx; | |
| const int ny = (int)by + delta.m_dy; | |
| // must reference a block inside this stripe (the encoder | |
| // never emits a cross-stripe reference); also keeps a | |
| // worker from reading another stripe's rows | |
| if (!stripe_tile.contains(nx, ny)) | |
| return false; // reference outside the stripe: malformed stream | |
| pEP_pred_blk = &log_blks(nx, ny); | |
| } | |
| if (!pEP_pred_blk) | |
| return false; | |
| // subset-1 references require a partitioned predictor | |
| if ((ep_pred_subset) && (pEP_pred_blk->m_num_partitions < 2)) | |
| return false; | |
| const bool fine = (log_blk.m_endpoint_bits[0] >= 6); | |
| for (uint32_t subset = 0; subset < log_blk.m_num_partitions; subset++) | |
| { | |
| uint8_t residuals[8]; | |
| uint32_t num_residuals = num_comps * 2; | |
| if ((!has_alpha) && (log_blk.m_mode == 6)) | |
| { | |
| assert(num_residuals == 8); | |
| num_residuals = 6; | |
| residuals[6] = 0; | |
| residuals[7] = 0; | |
| } | |
| for (uint32_t i = 0; i < num_residuals; i += 2) | |
| { | |
| const uint32_t chan = i >> 1; | |
| byte_cursor& strm = ep_deltas[(fine ? 0 : 4) + chan]; | |
| if (!strm.get(residuals[i + 0])) | |
| return false; | |
| if (!strm.get(residuals[i + 1])) | |
| return false; | |
| } | |
| uint8_t residual_pbits[2] = { 0, 0 }; | |
| for (uint32_t pb = 0; pb < fmt.m_num_pbits; pb++) | |
| { | |
| uint32_t v; | |
| if (!pbits.get_bits(1, v)) | |
| return false; | |
| residual_pbits[pb] = (uint8_t)v; | |
| } | |
| uint32_t num_residuals_out, num_residual_pbits_out; | |
| basist::bc7u::endpoint_dpcm(true, | |
| *pEP_pred_blk, ep_pred_subset, // mirrors the encoder (subset 1 for EP modes 6/7) | |
| log_blk, subset, | |
| residuals, num_residuals_out, residual_pbits, num_residual_pbits_out); | |
| if ((!has_alpha) && (log_blk.m_mode == 6)) | |
| { | |
| log_blk.m_endpoints[0][0][3] = 127; | |
| log_blk.m_endpoints[0][1][3] = 127; | |
| } | |
| } | |
| } | |
| // ---- weights: residual DCT (wt bit 1) or lossless residual DPCM (wt bit 0) ---- | |
| uint8_t pred_byte; | |
| if (!predictors.get(pred_byte)) | |
| return false; | |
| if (pred_byte >= cTotalCandidates * 4) | |
| return false; | |
| const uint32_t cand_index = pred_byte % cTotalCandidates; | |
| const uint32_t amp_code = pred_byte / cTotalCandidates; | |
| if ((amp_code) && (cand_index == cCandAbsolute)) | |
| return false; | |
| int weight_preds[16]; | |
| int* pWeight_predictions = nullptr; | |
| for (uint32_t p = 0; p < log_blk.m_num_planes; p++) | |
| { | |
| if (cand_index != cCandAbsolute) | |
| { | |
| // the decoder may reference ANY causal block (whole-image | |
| // tile): the encoder simply never emits a reference | |
| // that crosses one of its tile boundaries | |
| if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, stripe_tile, log_blks, p, weight_preds)) | |
| return false; // candidate unavailable here: malformed stream | |
| pWeight_predictions = weight_preds; | |
| } | |
| if (wt_mode != (uint32_t)xbc7_command_weight_mode::cCmdWeightDCT) | |
| { | |
| // ---- lossless residual DPCM weights ---- | |
| // Predictions are quantized to the plane's bit depth and | |
| // the wrapped n-bit index residuals (or, for the absolute | |
| // predictor, the raw indices) are read back. Exact. | |
| const uint32_t num_bits = log_blk.m_weight_bits[p]; | |
| const uint32_t mask = (1u << num_bits) - 1; | |
| byte_cursor& strm = (cand_index == cCandAbsolute) ? raw_weights : wt_resid[num_bits - 2]; | |
| uint8_t syms[16]; | |
| if (num_bits == 2) | |
| { | |
| for (uint32_t i = 0; i < 16; i += 4) | |
| { | |
| uint8_t b; | |
| if (!strm.get(b)) | |
| return false; | |
| syms[i + 0] = b & 3; | |
| syms[i + 1] = (b >> 2) & 3; | |
| syms[i + 2] = (b >> 4) & 3; | |
| syms[i + 3] = (uint8_t)(b >> 6); | |
| } | |
| } | |
| else | |
| { | |
| for (uint32_t i = 0; i < 16; i += 2) | |
| { | |
| uint8_t b; | |
| if (!strm.get(b)) | |
| return false; | |
| const uint8_t lo = b & 0xF, hi = (uint8_t)(b >> 4); | |
| if ((num_bits == 3) && ((lo > 7) || (hi > 7))) | |
| return false; // nibble bit 3 reserved-zero | |
| syms[i + 0] = lo; | |
| syms[i + 1] = hi; | |
| } | |
| } | |
| for (uint32_t i = 0; i < 16; i++) | |
| { | |
| const uint32_t pred_index = pWeight_predictions ? | |
| basist::bc7u::quant_weight(pWeight_predictions[i], num_bits) : 0; | |
| log_blk.m_weights[p][i] = (uint8_t)((syms[i] + pred_index) & mask); | |
| } | |
| continue; // next plane | |
| } | |
| // ---- DC ---- | |
| uint8_t dc_byte; | |
| if (!dc_coeffs.get(dc_byte)) | |
| return false; | |
| int dc = unpack_coeff_b(dc_byte); | |
| if (pred_byte != cCandAbsolute) // joint index 0 == absolute: DC is unsigned | |
| { | |
| uint32_t sign; | |
| if (!coeff_signs.get_bits(1, sign)) | |
| return false; | |
| if (sign) | |
| dc = -dc; | |
| } | |
| // ---- ACs ---- | |
| xbc7::dct_syms syms; | |
| syms.clear(); | |
| syms.m_dc = (int16_t)dc; | |
| uint32_t zig_idx = 1; | |
| while (zig_idx < 16) | |
| { | |
| uint8_t b; | |
| if (!ac_coeffs.get(b)) | |
| return false; | |
| if (b == 0xFF) | |
| { | |
| // trailing zeros to the end of the scan | |
| xbc7::coeff cf; | |
| cf.m_num_zeros = (int16_t)(16 - zig_idx); | |
| cf.m_coeff = INT16_MAX; | |
| syms.m_ac_vals.push_back(cf); | |
| break; | |
| } | |
| const uint32_t run = b; | |
| if ((zig_idx + run) > 15) | |
| return false; // a real coefficient must land at position <= 15 | |
| uint8_t mag_byte; | |
| if (!ac_coeffs.get(mag_byte)) | |
| return false; | |
| const int mag = unpack_coeff_b(mag_byte); | |
| if (!mag) | |
| return false; // zero coefficients are never coded | |
| uint32_t sign; | |
| if (!coeff_signs.get_bits(1, sign)) | |
| return false; | |
| xbc7::coeff cf; | |
| cf.m_num_zeros = (int16_t)run; | |
| cf.m_coeff = (int16_t)(sign ? -mag : mag); | |
| syms.m_ac_vals.push_back(cf); | |
| zig_idx += run + 1; | |
| } | |
| bool status = weight_grid_dct_fixed.inverse( | |
| basist::fixed16_16::from_int(global_q), p, pWeight_predictions, syms, log_blk, dct_work_fixed); | |
| if (!status) | |
| return false; | |
| } // p | |
| } | |
| // ---- single emit point: every block path (repeat-last, | |
| // repeat-upper, solid, config) falls through to here, so the | |
| // caller's block callback is invoked EXACTLY ONCE per block, | |
| // after the logical block is stored in m_log_blks. The caller may | |
| // pack a physical BC7 block, store it, compare it vs a reference, | |
| // transcode it, etc. A false return aborts the decode. ---- | |
| if (m_block_cb && !m_block_cb(bx, by, log_blk, m_block_data)) | |
| return false; | |
| } // bx | |
| } // by | |
| // ---- per-stripe desync tripwires: each stream consumed exactly its | |
| // range (cursors were bounded to [seek(s), seek(s+1))) ---- | |
| if (!commands.is_fully_consumed()) return false; | |
| if (!configs.is_fully_consumed()) return false; | |
| if (!partitions2.is_fully_consumed()) return false; | |
| if (!partitions3.is_fully_consumed()) return false; | |
| if (!predictors.is_fully_consumed()) return false; | |
| if (!dc_coeffs.is_fully_consumed()) return false; | |
| if (!ac_coeffs.is_fully_consumed()) return false; | |
| if (!solid_deltas.is_fully_consumed()) return false; | |
| if (!ep_block_index.is_fully_consumed()) return false; | |
| if (!raw_weights.is_fully_consumed()) return false; | |
| for (uint32_t i = 0; i < 3; i++) | |
| if (!wt_resid[i].is_fully_consumed()) return false; | |
| for (uint32_t i = 0; i < 8; i++) | |
| if (!ep_deltas[i].is_fully_consumed()) return false; | |
| if (!coeff_signs.is_fully_consumed()) return false; | |
| if (!pbits.is_fully_consumed()) return false; | |
| if (!ep_raw.is_fully_consumed()) return false; | |
| return true; | |
| } // decode_stripe | |
| bool image_unpacker::decode_tiny_mip() | |
| { | |
| for (uint32_t by = 0; by < m_num_blocks_y; by++) | |
| { | |
| for (uint32_t bx = 0; bx < m_num_blocks_x; bx++) | |
| { | |
| const uint8_t* p = m_tiny_blocks + ((size_t)by * m_num_blocks_x + bx) * 16; | |
| basist::bc7u::log_bc7_block log_blk; | |
| if (!basist::bc7u::unpack_bc7(p, log_blk)) | |
| return false; | |
| if (m_block_cb && !m_block_cb(bx, by, log_blk, m_block_data)) | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool image_unpacker::decode_all() | |
| { | |
| // guard here too: with no successful init(), m_num_stripes is 0 and the | |
| // loop would otherwise return true (false success) without decoding | |
| assert(m_initialized && "decode_all() called without a successful init()"); | |
| if (!m_initialized) | |
| return false; | |
| for (uint32_t s = 0; s < m_num_stripes; s++) | |
| if (!decode_stripe(s)) | |
| return false; | |
| return true; | |
| } | |
| // Single-threaded one-shot: prep + serial decode. | |
| bool unpack_image( | |
| const byte_span& comp, | |
| decode_init_callback_ptr pInit_callback, void* pInit_callback_data, | |
| decode_block_callback_ptr pBlock_callback, void* pBlock_callback_data) | |
| { | |
| #if !BASISD_SUPPORT_KTX2_ZSTD | |
| BASISU_NOTE_UNUSED(comp); BASISU_NOTE_UNUSED(pInit_callback); BASISU_NOTE_UNUSED(pInit_callback_data); | |
| BASISU_NOTE_UNUSED(pBlock_callback); BASISU_NOTE_UNUSED(pBlock_callback_data); | |
| BASISU_DEVEL_ERROR("xbc7::unpack_image: XBC7 decoding requires zstd (BASISD_SUPPORT_KTX2_ZSTD is 0)"); | |
| return false; | |
| #else | |
| image_unpacker dec; | |
| if (!dec.init(comp, pInit_callback, pInit_callback_data, pBlock_callback, pBlock_callback_data)) | |
| return false; | |
| return dec.decode_all(); | |
| #endif | |
| } | |
| // Threaded: caller owns `dec` (it must outlive the spawned jobs). init + spawn | |
| // one job per stripe via the spawner; returns false only on init failure and | |
| // does NOT wait. The caller waits on its own pool, then inspects results. | |
| bool unpack_image_threaded( | |
| image_unpacker& dec, | |
| const byte_span& comp, | |
| job_spawner& spawner, | |
| decode_init_callback_ptr pInit_callback, void* pInit_callback_data, | |
| decode_block_callback_ptr pBlock_callback, void* pBlock_callback_data) | |
| { | |
| #if !BASISD_SUPPORT_KTX2_ZSTD | |
| BASISU_NOTE_UNUSED(dec); BASISU_NOTE_UNUSED(comp); BASISU_NOTE_UNUSED(spawner); | |
| BASISU_NOTE_UNUSED(pInit_callback); BASISU_NOTE_UNUSED(pInit_callback_data); | |
| BASISU_NOTE_UNUSED(pBlock_callback); BASISU_NOTE_UNUSED(pBlock_callback_data); | |
| BASISU_DEVEL_ERROR("xbc7::unpack_image_threaded: XBC7 decoding requires zstd (BASISD_SUPPORT_KTX2_ZSTD is 0)"); | |
| return false; | |
| #else | |
| if (!dec.init(comp, pInit_callback, pInit_callback_data, pBlock_callback, pBlock_callback_data)) | |
| return false; | |
| const uint32_t num_stripes = dec.get_num_stripes(); | |
| for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) | |
| spawner.spawn_job(dec, stripe_index); | |
| return true; | |
| #endif | |
| } | |
| } // namespace xbc7 | |
| } // namespace basist |