/* gamma_demo.c —— **3D 渲染能力第一批**（伽马/色调映射 · alpha test · mipmap+采样器）示例 + 判据。
 * gamma_demo.c — **first batch of 3D render features** (gamma/tone mapping, alpha test,
 * mipmap+sampler): example and criteria.
 *
 * ## 这个文件覆盖三样（按序号 `#649`–`#651`）
 * ## This file covers three things (numbers #649-#651)
 *
 *   ① `ui_3d_viewport_set_gamma(vp, mode)`      `#649` —— 输出变换（sRGB / Reinhard / ACES）
 *   1) `ui_3d_viewport_set_gamma(vp, mode)`     `#649` - output transform (sRGB / Reinhard / ACES)
 *   ② `ui_3d_obj_alpha_test(obj, cutoff)`       `#650` —— 镂空（glTF 的 `alphaMode: MASK`）
 *   2) `ui_3d_obj_alpha_test(obj, cutoff)`      `#650` - alpha test (glTF's alphaMode: MASK)
 *   ③ `ui_3d_obj_sampler(obj,ws,wt,min,mag)`    `#651` —— mipmap + 过滤 + 平铺
 *   3) `ui_3d_obj_sampler(obj,ws,wt,min,mag)`   `#651` - mipmap + filtering + wrap
 *
 * ## ⚠⚠ 三样**默认全关**，而这个文件必须能证明「关着时真的没动」
 * ## ⚠⚠ All three are **off by default**, and this file must prove "off really means untouched"
 *
 * 本仓对老程序有一条硬保证：只调过 `#612` 的程序画面**逐位不变**。
 * The engine guarantees old programs are **bit-identical**.
 * 这三样都会改像素（伽马改全屏、mipmap 改远景、alpha test 改镂空），所以每段判据都配一条**反向**断言。
 * Each of the three changes pixels (gamma the frame, mipmap the distance, alpha test the cut-outs), so every
 * section carries a **reverse** assertion.
 * ——「把它设回默认，读数必须逐位回到原样」。
 * Set it back to the default and the reading must return bit-for-bit.
 *
 * ## 为什么判据挑的是这些量（而不是"有没有那个接口"）
 * ## Why these particular measurements (and not "does the entry point exist")
 *
 *   · **伽马**：不能只断言"变亮了" —— 整体乘 1.5 也叫变亮。
 *   · **gamma**: "it got brighter" is not enough - multiplying by 1.5 also brightens.
 *     这里拿**实测的关闭值**套一遍 sRGB 分段曲线，要求落在 ±2 以内。
 *     The measured off-value is run through the sRGB piecewise curve and must land within +-2.
 *     再断言**暗部抬升 > 亮部抬升**（凹曲线的签名，线性提亮做不到这一点）。
 *     On top of that the **dark lift must exceed the bright lift** (the signature of a concave curve,
 *     which a linear brighten cannot produce).
 *   · **alpha test**：必须是**镂空**（露出后面的东西），不是"物体变透明"。
 *   · **alpha test**: it must be a **cut-out** (what is behind shows through), not "the object went translucent".
 *     所以前面那张方片 alpha=0、后面一张实心：关掉 cutoff 读到前面、打开读到后面。
 *     So a front quad at alpha 0 sits in front of a solid back quad: cutoff off reads the front, cutoff on
 *     reads the back.
 *   · **mipmap**：断言的是**缩小时的取值离散度下降**，不是"调了 generateMipmap"。
 *   · **mipmap**: the criterion is that the **spread collapses when minified**, not that "generateMipmap was called".
 *     关着时小方片上是黑/白相间的点采样（离散度大），开着时是 mip 层给出的平均值（离散度小）。
 *     With mips off a tiny quad shows point-sampled black/white cells (large spread); with mips on it shows
 *     the averaged mip level (small spread).
 *   · **采样器**：U 方向跑到 `[1,2]` 的网格 + 一张横向渐变图 ⇒ 重复会看到整条渐变。
 *   · **sampler**: a grid whose U runs over [1,2] plus a horizontal gradient texture: repeat shows the whole ramp.
 *     钳边会把整片糊成最后一列的颜色 —— 两者在同一张图上**一眼分得开**。
 *     Clamp smears the last column over everything - unmistakable on the same quad.
 *
 * ## 跑法
 * ## How to run
 *   dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll \
 *          third_party/vml/Examples/c/3d/gamma_demo.c --screen 393x757
 *   手机：命令行页敲 `vml run examples/c/3d/gamma_demo.c`
 *   Phone: on the command-line page type `vml run examples/c/3d/gamma_demo.c`
 *
 * 机器判读：逐行 `PASS …` / `FAIL …`，末行 `GAMMA_DEMO PASS|FAIL (fails=N)`。
 * Machine-readable: one `PASS ...` / `FAIL ...` line each, ending with `GAMMA_DEMO PASS|FAIL (fails=N)`.
 */

#include <vml_ui.h>
#include <vml_3d.h>
#include <math.h>     /* ⚠ 见 g3_all.c 的文件头：漏了它 sin/cos 静默返回垃圾，而程序不报错 */
                      /* See the g3_all.c header: without it sin/cos silently return garbage. */
#include <stdio.h>

/* ══ 记分 ══════════════════════════════════════════════════════════════════ */
/* == Scoring == */

int g_fails;

void ck(char* tag, int cond, int a, int b) {
    if (cond) { printf("PASS %s %d %d\n", tag, a, b); }
    else { g_fails = g_fails + 1; printf("FAIL %s %d %d\n", tag, a, b); }
}

int rch(int c) { return (c >> 16) & 0xFF; }
int gch(int c) { return (c >> 8) & 0xFF; }
int bch(int c) { return c & 0xFF; }

/* ══ 网格装配（pos3 + nrm3 + uv2 = 8 个 float32 = 32 字节/顶点，紧随其后是 int32 索引）════
 * == Mesh assembly (pos3 + nrm3 + uv2 = 8 float32 = 32 bytes per vertex, then int32 indices) == */

float MB[256];
int g_vn;
int g_in;

void mb_vertex(int i, float px, float py, float pz,
               float nx, float ny, float nz, float u, float v) {
    int b = i * 8;
    MB[b + 0] = px; MB[b + 1] = py; MB[b + 2] = pz;
    MB[b + 3] = nx; MB[b + 4] = ny; MB[b + 5] = nz;
    MB[b + 6] = u;  MB[b + 7] = v;
}
void mb_index(int k, int v) { int* ip; ip = (int*)&MB[0]; ip[g_vn * 8 + k] = v; }
int mb_ptr(void) { return (int)&MB[0]; }

/* 一个正对 +Z 的方片（边长 2、中心在原点），**四角 uv 由参数给**。
 * A quad facing +Z (side 2, centred at the origin); the four corner uvs come from the arguments.
 * ⚠ uv 必须**四角互不相同** —— 全 0 的网格上"贴图没接上"与"贴了纯色图"读出来一模一样（见 tex_demo）。
 * ⚠ The four corners must be **distinct** - on an all-zero grid a broken uv attribute reads exactly
 *   like a solid-colour texture (see tex_demo).
 * ⚠ `u1` 可以**大于 1**：`#651` 的平铺模式判据就靠"uv 跑到 [1,2]"这一段（见文件头）。 */
/*   `u1` may be **greater than 1**: the wrap-mode criteria in #651 rely on uv running over [1,2]. */
void gen_quad(float u0, float v0, float u1, float v1) {
    mb_vertex(0, -1.0f, -1.0f, 0.0f,  0.0f, 0.0f, 1.0f,  u0, v1);
    mb_vertex(1,  1.0f, -1.0f, 0.0f,  0.0f, 0.0f, 1.0f,  u1, v1);
    mb_vertex(2,  1.0f,  1.0f, 0.0f,  0.0f, 0.0f, 1.0f,  u1, v0);
    mb_vertex(3, -1.0f,  1.0f, 0.0f,  0.0f, 0.0f, 1.0f,  u0, v0);
    g_vn = 4;
    mb_index(0, 0); mb_index(1, 1); mb_index(2, 2);
    mb_index(3, 0); mb_index(4, 2); mb_index(5, 3);
    g_in = 6;
}

/* ══ 取样 ══════════════════════════════════════════════════════════════════ */
/* == Sampling == */

int sx(int sc) { return sc & 0xFFFF; }
int sy(int sc) { return (sc >> 16) & 0xFFFF; }

/* n×n 网格里的**红通道离散度**（max - min）：min==max 表示这一片是均匀的。
 * The **red-channel spread** (max - min) over an n-by-n grid: min == max means the patch is uniform.
 * ⚠ 用**离散度**而不是方差：判据要的是"还有没有黑/白在跳"，而方差会被一格极值拉爆。
 * ⚠ Spread rather than variance: the question is "are black and white still alternating", and a
 *   variance would be dominated by a single extreme sample. */
int spread_red(int vp, int cx, int cy, int half, int step) {
    int i; int j; int lo; int hi; int v;
    lo = 999; hi = -1;
    for (j = -half; j <= half; j = j + 1)
        for (i = -half; i <= half; i = i + 1) {
            v = rch(ui_3d_viewport_get_pixel(vp, cx + i * step, cy + j * step));
            if (v < lo) lo = v;
            if (v > hi) hi = v;
        }
    return hi - lo;
}

/* sRGB 规范的分段曲线（**与三端着色器里那段是同一个式子**）。
 * The sRGB piecewise curve (**the very same expression as the one in all three shaders**).
 * 判据拿它当期望值 ⇒ 只对得上"真的做了曲线变换"的，对不上"整体变亮"的。
 * The criterion uses it as the expectation, so only a real curve transform matches, not a linear brighten. */
float srgb_curve(float x) {
    if (x <= 0.0031308f) return 12.92f * x;
    return 1.055f * pow(x, 1.0f / 2.4f) - 0.055f;
}

int main(void) {
    int w; int h; int vp;
    int mesh01; int mesh12;
    int o_dark; int o_lite; int o_front; int o_back; int o_mip; int o_wrap;
    int img_check; int img_grad;
    int r; int i; int j; int sc; int px; int py;
    int d0; int l0; int d1; int l1; int d2; int l2; int dback;
    int exp_d; int exp_l;
    int sp_nomip; int sp_mips; int sp_nomip2; int sp_big;
    int halfpx;
    int wx; int wy; int wc; int wl; int wc2; int wl2;

    g_fails = 0;

    ui_win_open_ex("3D 渲染能力示例", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    w = ui_scr_get_w_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
    h = ui_scr_get_h_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
    printf("SCR=%dx%d\n", w, h);
    if (w < 64 || h < 64) { printf("GAMMA_DEMO FAIL (fails=99)\n"); return 99; }

    /* ── ① 两张贴图：64×64 单像素棋盘（给 mipmap 用）、64×8 横向渐变（给平铺用）─────
     * -- 1) Two textures: a 64x64 one-pixel checkerboard (for mipmap) and a 64x8 horizontal
     *       gradient (for wrap) --
     * ⚠ 棋盘**必须是单像素格**：mip 每一级把 2×2 平均一次，单像素格在**第一级**就精确平均成 50% 灰。
     * ⚠ The checkerboard must use **one-pixel cells**: each mip level averages 2x2, so one-pixel cells average
     *   to exactly 50% grey already at level 1.
     * ⇒ 只要 LOD ≥ 1，采样值就是均匀灰（判据因此可以把阈值卡到个位数）。
     * => Any LOD of 1 or more samples a uniform grey (hence the single-digit threshold).
     * ⚠⚠ **尺寸不能太小**（实测踩到）：小方片盖住 ≈22 像素，32×32 的图在那里的 LOD 只有 **0.55**。
     * ⚠⚠ The texture must **not be too small** (measured): the small quad covers about 22 pixels, where a
     *   32x32 texture only reaches LOD **0.55**.
     * ⇒ 三线性会混进一半 level 0（满对比棋盘），离散度只从 233 掉到 **106**，看着像"mipmap 没生效"。
     * => Trilinear then still blends in half of level 0 (the full-contrast checkerboard), so the spread only
     *   drops 233 -> **106**, which reads like "mipmap did not work".
     * 64×64 把同一个方片的 LOD 抬到 **1.55** ⇒ 混的是 level 1 与 2，**两级都已经是精确的 50% 灰**。
     * 64x64 lifts the same quad to LOD **1.55**, blending levels 1 and 2 - both already exact 50% grey. */
    for (j = 0; j < 64; j = j + 1)
        for (i = 0; i < 64; i = i + 1) {
            int c = 0xFF000000;
            if ((i + j) % 2 == 0) c = 0xFFFFFFFF;
            ui_2d_rect(8 + i, 8 + j, 1, 1, c, 1, 0, 0);
        }
    /* 横向渐变：R 从 0 到 252、G 从 255 到 3 ⇒ **左绿右红**，与蓝色背景一眼分得开。 */
    /* Horizontal gradient: R runs 0 -> 252 while G runs 255 -> 3, i.e. **green on the left, red on
     * the right**, unmistakable against the blue background. */
    /* ⚠⚠ **画在棋盘下方 16 像素处**（y=88），不能贴着它。
     * ⚠⚠ **Drawn 16 pixels below the checkerboard** (y=88), never touching it.
     *   实测踩到：棋盘画到 64×64 之后，原先贴在 y=60 的渐变**压住了棋盘最下面 8 行**。
     *   Measured: once the checkerboard grew to 64x64, the gradient at y=60 **covered its bottom 8 rows**.
     *   于是抓下来的"棋盘"底部混进一条彩色条，mip 的层平均不再均匀。
     *   The captured "checkerboard" then had a coloured stripe along the bottom and the mip levels were no longer uniform.
     *   ⇒ 判据读到离散度 45 而不是 0（看着像"mipmap 没生效"）。
     *   => The criterion read a spread of 45 instead of 0 (which looks like "mipmap did not work"). */
    for (i = 0; i < 64; i = i + 1) {
        int rr = i * 4;
        int gg = 255 - i * 4;
        ui_2d_rect(8 + i, 88, 1, 8, 0xFF000000 | (rr << 16) | (gg << 8), 1, 0, 0);
    }
    ui_2d_present();
    img_check = ui_2d_get_image(8, 8, 64, 64);
    img_grad = ui_2d_get_image(8, 88, 64, 8);
    ck("checker texture handle", img_check > 0, img_check, 0);
    ck("gradient texture handle", img_grad > 0, img_grad, 0);

    /* ⚠ 抓完图**必须把 2D 层清成透明**：不清的话 2D 层盖在 3D 视口上面，屏幕一片黑（tex_demo 实测）。 */
    /* ⚠ After grabbing, the 2D layer **must be cleared to transparent**, otherwise it covers the 3D
     * viewport and the screen goes black (measured in tex_demo). */
    ui_2d_clear(0);
    ui_2d_present();

    /* ── ② 视口 + 相机 + 六个物体 ───────────────────────────────────────────
     * -- 2) Viewport + camera + six objects --
     * 相机在 (0,0,3) 看向原点、竖直张角 60° ⇒ 距离 3 处**每世界单位 ≈ (h/2)/tan(30°)** 像素。
     * The camera sits at (0,0,3) looking at the origin with a 60-degree **vertical** angle, so at
     * distance 3 one world unit is about (h/2)/tan(30) pixels. */
    vp = ui_3d_viewport_create(0, 0, w, h, 0);
    ck("viewport -> handle", vp > 0, vp, 0);
    ui_3d_viewport_set_camera(vp, 0.0f, 0.0f, 3.0f, 0.0f, 0.0f, 0.0f);
    ui_3d_viewport_set_fov(vp, 60.0f, 0.1f, 100.0f);
    ui_3d_viewport_set_bg(vp, 0xFF1010C0);   /* 蓝底：渐变图里 B=0 ⇒ 判"这一格是不是物体"只看 B */
                                             /* Blue background: the gradient has B=0, so "is this
                                              * pixel on the object" is decided by B alone. */
    ui_3d_viewport_set_light(vp, 0.0f, 0.0f, 1.0f, 0xFFFFFFFF, 0.35f);

    gen_quad(0.0f, 0.0f, 1.0f, 1.0f);
    mesh01 = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    ck("mesh uv[0,1] -> handle", mesh01 > 0, mesh01, 0);
    gen_quad(1.0f, 0.0f, 2.0f, 1.0f);
    mesh12 = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
    ck("mesh uv[1,2] -> handle", mesh12 > 0, mesh12, 0);

    /* 六个物体都开**自发光** ⇒ 片元直接输出基色/纹理原色，判据的期望值才是可算的。 */
    /* All six objects are **emissive**, so the fragment outputs the base/texture colour directly and
     * the expected values stay computable. */
    o_dark = ui_3d_obj_create(vp, mesh01);
    ui_3d_obj_set_color(o_dark, 0xFF404040);
    ui_3d_obj_set_emissive(o_dark, 1);
    ui_3d_obj_set_scale(o_dark, 0.25f);
    ui_3d_obj_set_pos(o_dark, -0.6f, 1.35f, 0.0f);

    o_lite = ui_3d_obj_create(vp, mesh01);
    ui_3d_obj_set_color(o_lite, 0xFFC0C0C0);
    ui_3d_obj_set_emissive(o_lite, 1);
    ui_3d_obj_set_scale(o_lite, 0.25f);
    ui_3d_obj_set_pos(o_lite, 0.6f, 1.35f, 0.0f);

    o_front = ui_3d_obj_create(vp, mesh01);
    ui_3d_obj_set_color(o_front, 0x00FFFFFF);   /* **alpha = 0** ⇒ 关掉 cutoff 也照样画出来 */
                                                /* **alpha = 0**: without a cutoff it still draws. */
    ui_3d_obj_set_emissive(o_front, 1);
    ui_3d_obj_set_scale(o_front, 0.30f);
    ui_3d_obj_set_pos(o_front, 0.0f, 0.45f, 0.35f);   /* 更靠近相机 ⇒ 挡住后面那张 */
                                                      /* Closer to the camera, so it occludes the back quad. */

    o_back = ui_3d_obj_create(vp, mesh01);
    ui_3d_obj_set_color(o_back, 0xFF20C040);    /* 实心绿：镂空之后必须**看见它** */
                                                /* Solid green: it must be **seen** through the hole. */
    ui_3d_obj_set_emissive(o_back, 1);
    ui_3d_obj_set_scale(o_back, 0.45f);
    ui_3d_obj_set_pos(o_back, 0.0f, 0.45f, 0.0f);

    o_wrap = ui_3d_obj_create(vp, mesh12);
    ui_3d_obj_set_color(o_wrap, 0xFFFFFFFF);    /* 白色是乘性 tint 的中性元 */
                                                /* White is the neutral element of the tint. */
    ui_3d_obj_set_emissive(o_wrap, 1);
    ui_3d_obj_set_scale(o_wrap, 0.25f);
    ui_3d_obj_set_pos(o_wrap, 0.0f, -1.35f, 0.0f);
    r = ui_3d_obj_set_tex(o_wrap, img_grad);
    ck("gradient texture mapped", r == 0, r, 0);

    o_mip = ui_3d_obj_create(vp, mesh01);
    ui_3d_obj_set_color(o_mip, 0xFFFFFFFF);
    ui_3d_obj_set_emissive(o_mip, 1);
    ui_3d_obj_set_scale(o_mip, 0.05f);          /* 先小：**缩小**才会走到 mip 层 */
                                                /* Small at first: only **minification** reaches the mip levels. */
    ui_3d_obj_set_pos(o_mip, 0.0f, -0.45f, 0.0f);
    r = ui_3d_obj_set_tex(o_mip, img_check);
    ck("checker texture mapped", r == 0, r, 0);

    /* 每个物体报出来的屏幕中心 —— 判据一律以它为中心取样（**不按屏幕比例猜**，见 tex_demo）。 */
    /* Sample around the screen centre each object reports - **never guess from screen proportions**
     * (see tex_demo). */
    sc = ui_3d_obj_get_screen(o_dark);  px = sx(sc); py = sy(sc);
    sc = ui_3d_obj_get_screen(o_lite);  printf("LITE=%d,%d\n", sx(sc), sy(sc));
    sc = ui_3d_obj_get_screen(o_front); printf("FRONT=%d,%d\n", sx(sc), sy(sc));
    sc = ui_3d_obj_get_screen(o_mip);   printf("MIP=%d,%d\n", sx(sc), sy(sc));
    sc = ui_3d_obj_get_screen(o_wrap);  printf("WRAP=%d,%d\n", sx(sc), sy(sc));
    (void)r; (void)j;

    /* ══ ③ 伽马 / 色调映射（`#649`）═════════════════════════════════════════ */
    /* == 3) Gamma / tone mapping (#649) == */
    sc = ui_3d_obj_get_screen(o_dark);  px = sx(sc); py = sy(sc);
    ui_2d_present();
    d0 = ui_3d_viewport_get_pixel(vp, px, py);
    sc = ui_3d_obj_get_screen(o_lite);
    l0 = ui_3d_viewport_get_pixel(vp, sx(sc), sy(sc));
    printf("GAMMA_OFF dark=%d,%d,%d light=%d,%d,%d\n",
           rch(d0), gch(d0), bch(d0), rch(l0), gch(l0), bch(l0));
    ck("gamma off: dark quad is dark", rch(d0) > 40 && rch(d0) < 90, rch(d0), 64);
    ck("gamma off: light quad is light", rch(l0) > 160 && rch(l0) < 220, rch(l0), 192);

    r = ui_3d_viewport_set_gamma(vp, 1);
    ck("set_gamma(1) rc", r == 0, r, 0);
    ui_2d_present();
    d1 = ui_3d_viewport_get_pixel(vp, px, py);
    l1 = ui_3d_viewport_get_pixel(vp, sx(ui_3d_obj_get_screen(o_lite)), sy(ui_3d_obj_get_screen(o_lite)));
    printf("GAMMA_SRGB dark=%d light=%d\n", rch(d1), rch(l1));
    ck("sRGB changes both quads (not a no-op)", rch(d1) != rch(d0) && rch(l1) != rch(l0), rch(d1), rch(l1));

    /* 期望值**由实测的关闭值现算**（不是写死 138/225）：换了屏幕尺寸、换了驱动也照样成立。 */
    /* The expectation is **computed from the measured off-value**, not hard-coded: it holds on any
     * screen size and any driver. */
    exp_d = (int)(srgb_curve((float)rch(d0) / 255.0f) * 255.0f + 0.5f);
    exp_l = (int)(srgb_curve((float)rch(l0) / 255.0f) * 255.0f + 0.5f);
    ck("sRGB matches the piecewise curve on the dark quad", rch(d1) >= exp_d - 2 && rch(d1) <= exp_d + 2, rch(d1), exp_d);
    ck("sRGB matches the piecewise curve on the light quad", rch(l1) >= exp_l - 2 && rch(l1) <= exp_l + 2, rch(l1), exp_l);
    /* ★ 这一条才是"真的做了曲线"的签名：**凹曲线上暗部抬得比亮部多**（线性提亮恰好相反）。 */
    /* ★ This is the signature of a real curve: on a concave curve the **dark lift exceeds the bright
     *   lift**, while a linear brighten does exactly the opposite. */
    ck("curve signature: dark lifts more than light",
       (rch(d1) - rch(d0)) > (rch(l1) - rch(l0)), rch(d1) - rch(d0), rch(l1) - rch(l0));

    /* 色调映射（Reinhard）：**高光被压回去** —— 亮块的值必须降到只做 sRGB 之下。 */
    /* Tone mapping (Reinhard): **highlights get pushed back down** - the bright quad must end up
     * below the sRGB-only value. */
    r = ui_3d_viewport_set_gamma(vp, 2);
    ck("set_gamma(2) rc", r == 0, r, 0);
    ui_2d_present();
    l2 = ui_3d_viewport_get_pixel(vp, sx(ui_3d_obj_get_screen(o_lite)), sy(ui_3d_obj_get_screen(o_lite)));
    d2 = ui_3d_viewport_get_pixel(vp, px, py);
    printf("GAMMA_REINHARD dark=%d light=%d\n", rch(d2), rch(l2));
    ck("Reinhard compresses the bright quad below sRGB-only", rch(l2) < rch(l1), rch(l2), rch(l1));
    ck("Reinhard still applied a real transform", rch(l2) != rch(l0), rch(l2), rch(l0));

    r = ui_3d_viewport_set_gamma(vp, 9);
    ck("gamma mode out of range is rejected", r < 0, r, -2);

    /* 反向：设回 0 ⇒ **逐位**回到最初那两个读数（"关着时一个像素都不许变"这条硬保证）。 */
    /* Reverse: back to 0 and the two readings must return **bit for bit** (the "off changes not one
     * pixel" guarantee). */
    r = ui_3d_viewport_set_gamma(vp, 0);
    ui_2d_present();
    dback = ui_3d_viewport_get_pixel(vp, px, py);
    printf("GAMMA_BACK dark=%d\n", rch(dback));
    ck("gamma off again -> dark quad is bit-identical", dback == d0, dback, d0);

    /* ══ ④ 镂空 alpha test（`#650`）═════════════════════════════════════════ */
    /* == 4) Alpha test (#650) == */
    sc = ui_3d_obj_get_screen(o_front); px = sx(sc); py = sy(sc);
    ui_2d_present();
    wc = ui_3d_viewport_get_pixel(vp, px, py);
    printf("ALPHA_OFF centre=%d,%d,%d\n", rch(wc), gch(wc), bch(wc));
    ck("cutoff off: the front quad (alpha 0) is still drawn",
       rch(wc) > 200 && gch(wc) > 200 && bch(wc) > 200, rch(wc), gch(wc));

    r = ui_3d_obj_alpha_test(o_front, 0.5f);
    ck("alpha_test rc", r == 0, r, 0);
    ui_2d_present();
    wl = ui_3d_viewport_get_pixel(vp, px, py);
    printf("ALPHA_ON centre=%d,%d,%d\n", rch(wl), gch(wl), bch(wl));
    /* ★ 镂空的签名：**读到的是后面那张绿方片** —— 不是"变半透明"，是这一块像素真的没了。 */
    /* ★ The cut-out signature: the reading is the **green quad behind** - not "went translucent", the
     *   pixels really are gone. */
    ck("cutout reveals the quad behind", gch(wl) > 150 && rch(wl) < 80, gch(wl), rch(wl));

    /* 反向之一：把前面那张的 alpha 设回 255 ⇒ 同一个 cutoff 下它**必须重新出现**（阈值没乱杀）。 */
    /* Reverse 1: give the front quad alpha 255 and it must **come back** at the same cutoff (the
     * threshold is not simply killing everything). */
    /* ⚠ 比的是**三个颜色通道**、不是整包 32 位：alpha=0 与 alpha=255 的**帧缓冲 alpha 不同**
     * ⚠ Compare the **three colour channels**, not the packed 32-bit word: alpha 0 and alpha 255 leave
     *   （0x00FFFFFF vs 0xFFFFFFF），拿整包比会假红 —— 而 RGB 相同恰恰说明"这一块又是那张方片了"。
     *   **different framebuffer alphas** (0x00FFFFFF vs 0xFFFFFFFF) so a packed compare goes falsely
     *   red - while equal RGB is exactly the proof that the front quad is back. */
    ui_3d_obj_set_color(o_front, 0xFFFFFFFF);
    ui_2d_present();
    wc2 = ui_3d_viewport_get_pixel(vp, px, py);
    ck("alpha 255 survives the same cutoff (front quad is back)",
       rch(wc2) == rch(wc) && gch(wc2) == gch(wc) && bch(wc2) == bch(wc), bch(wc2), bch(wc));
    ui_3d_obj_set_color(o_front, 0x00FFFFFF);

    /* 反向之二：cutoff 设回 0 ⇒ 画面**逐位**回到打开之前那一帧。 */
    /* Reverse 2: cutoff back to 0 and the picture returns **bit for bit**. */
    r = ui_3d_obj_alpha_test(o_front, 0.0f);
    ui_2d_present();
    wl2 = ui_3d_viewport_get_pixel(vp, px, py);
    ck("cutoff off again -> bit-identical", wl2 == wc, wl2, wc);

    /* ══ ⑤ 采样器：平铺模式（`#651`）═══════════════════════════════════════ */
    /* == 5) Sampler: wrap mode (#651) == */
    /* 渐变图 + uv∈[1,2]：**重复**会看到整条渐变（左绿右红），**钳边**把最后一列糊满整片（全红）。 */
    /* Gradient texture with uv in [1,2]: **repeat** shows the whole ramp (green left, red right) while
     * **clamp** smears the last column across everything (all red). */
    sc = ui_3d_obj_get_screen(o_wrap);
    wx = sx(sc); wy = sy(sc);
    halfpx = (int)((float)h * 0.5f / (3.0f * 0.57735f) * 0.25f);   /* 方片半宽（像素） */
                                                                   /* Half-width of the quad in pixels. */
    printf("WRAP centre=%d,%d halfpx=%d\n", wx, wy, halfpx);
    ui_2d_present();
    wc = ui_3d_viewport_get_pixel(vp, wx, wy);
    wl = ui_3d_viewport_get_pixel(vp, wx - halfpx * 7 / 10, wy);
    printf("WRAP_REPEAT centre=%d,%d,%d left=%d,%d,%d\n", rch(wc), gch(wc), bch(wc), rch(wl), gch(wl), bch(wl));
    /* 先证明取样点**真的落在物体上**（B 通道只在物体上是 0）—— 否则下面那条判据是"隔着背景比的"。 */
    /* First prove the probe is **really on the object** (only there is the B channel 0); otherwise the
     * next assertion compares background with background. */
    ck("repeat: both probes are on the quad", bch(wc) < 64 && bch(wl) < 64, bch(wc), bch(wl));
    ck("repeat: the ramp runs left-to-right", rch(wc) - rch(wl) > 60, rch(wc), rch(wl));

    r = ui_3d_obj_sampler(o_wrap, 1, 1, 1, 1);     /* wrapS/T = 1 ⇒ 钳到边缘 */
                                                   /* wrapS/T = 1 means clamp to edge */
    ck("obj_sampler rc", r == 0, r, 0);
    ui_2d_present();
    wc2 = ui_3d_viewport_get_pixel(vp, wx, wy);
    wl2 = ui_3d_viewport_get_pixel(vp, wx - halfpx * 7 / 10, wy);
    printf("WRAP_CLAMP centre=%d,%d,%d left=%d,%d,%d\n", rch(wc2), gch(wc2), bch(wc2), rch(wl2), gch(wl2), bch(wl2));
    ck("clamp: the quad becomes uniform", rch(wc2) - rch(wl2) < 12 && rch(wc2) - rch(wl2) > -12, rch(wc2), rch(wl2));
    ck("clamp: the left probe is brighter than under repeat", rch(wl2) > rch(wl) + 60, rch(wl2), rch(wl));

    r = ui_3d_obj_sampler(o_wrap, 0, 0, 1, 1);     /* 设回默认 */
                                                   /* back to the defaults */
    ui_2d_present();
    ck("wrap back to repeat -> bit-identical", ui_3d_viewport_get_pixel(vp, wx, wy) == wc, ui_3d_viewport_get_pixel(vp, wx, wy), wc);
    r = ui_3d_obj_sampler(o_wrap, 3, 0, 1, 1);
    ck("wrap out of range is rejected", r < 0, r, -2);
    r = ui_3d_obj_sampler(o_wrap, 0, 0, 7, 1);
    ck("minFilter out of range is rejected", r < 0, r, -2);

    /* ══ ⑥ mipmap（`#651`）════════════════════════════════════════════════ */
    /* == 6) mipmap (#651) == */
    /* 小方片（scale 0.05）盖住 ≈22 像素，而贴图是 64 格 ⇒ 每个屏幕像素盖住约 3 个纹素（LOD ≈ 1.55）。 */
    /* The small quad (scale 0.05) covers about 22 pixels while the texture has 64 cells, so each screen
     * pixel covers roughly 3 texels (LOD about 1.55). */
    sc = ui_3d_obj_get_screen(o_mip);
    px = sx(sc); py = sy(sc);
    printf("MIP centre=%d,%d\n", px, py);
    sp_nomip = spread_red(vp, px, py, 3, 2);
    printf("MIP_NOMIP spread=%d\n", sp_nomip);
    /* ★ 关着 mip 时**必须有离散度** —— 这一条同时是"贴图真的贴上了、而且高频"的取证。 */
    /* ★ With mips off there **must** be a spread - which doubles as proof that the texture really is
     *   mapped and really is high frequency. */
    ck("no mipmap: the minified checkerboard still alternates", sp_nomip >= 40, sp_nomip, 40);

    r = ui_3d_obj_sampler(o_mip, 0, 0, 5, 1);      /* minFilter = 5 ⇒ 三线性（要 mip） */
                                                   /* minFilter = 5 means trilinear (needs mips) */
    ck("sampler(trilinear) rc", r == 0, r, 0);
    sp_mips = spread_red(vp, px, py, 3, 2);
    printf("MIP_MIPS spread=%d\n", sp_mips);
    /* ★ 这一条才是"mipmap 生效"：**缩小时的离散度塌下去**（点采样 → 层平均）。 */
    /* ★ This is what "mipmap works" means: the **spread collapses when minified** (point sampling
     *   becomes a level average). */
    ck("mipmap: the spread collapses when minified", sp_mips < sp_nomip && sp_mips <= 24, sp_mips, sp_nomip);

    /* 反向：minFilter 设回 1（线性、无 mip）⇒ 离散度**必须原样回来**。 */
    /* Reverse: minFilter back to 1 (linear, no mips) and the spread must **come back**. */
    r = ui_3d_obj_sampler(o_mip, 0, 0, 1, 1);
    ui_2d_present();
    sp_nomip2 = spread_red(vp, px, py, 3, 2);
    printf("MIP_NOMIP_AGAIN spread=%d\n", sp_nomip2);
    ck("mipmap off again -> spread comes back", sp_nomip2 == sp_nomip, sp_nomip2, sp_nomip);

    /* 放大时**不该**有 mip 参与：大方片上两种 minFilter 必须给出同一片像素（"只在该变的地方变"）。 */
    /* Magnification must **not** involve mips: on the big quad both minFilters must give the same
     * patch (change only where it should change). */
    ui_3d_obj_set_scale(o_mip, 1.0f);
    ui_2d_present();
    sc = ui_3d_obj_get_screen(o_mip);
    sp_big = spread_red(vp, sx(sc), sy(sc), 2, 1);
    ui_3d_obj_sampler(o_mip, 0, 0, 5, 1);
    ui_2d_present();
    printf("MIP_BIG linear=%d trilinear=%d\n", sp_big, spread_red(vp, sx(sc), sy(sc), 2, 1));
    ck("magnification is identical with and without mipmap",
       sp_big == spread_red(vp, sx(sc), sy(sc), 2, 1), sp_big, spread_red(vp, sx(sc), sy(sc), 2, 1));

    if (g_fails == 0) printf("GAMMA_DEMO PASS (fails=0)\n");
    else printf("GAMMA_DEMO FAIL (fails=%d)\n", g_fails);
    return g_fails;
}
