OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_brush_identity.cpp

125 lines · cpp ref:
// ============================================================================
// BRUSH IDENTITY — a brush must be nameable across peers, not just locally.
//
// hyper-games #22 §5 item 1. Faces got CRDT dots (test_brush_topology.cpp);
// brushes did not. `b.id = next_brush_id_++` is a LOCAL counter, and the durable
// view is a last-write-wins KV projection rather than an ordered op stream — so
// two peers each creating a brush allocate THE SAME id for DIFFERENT geometry,
// with nothing anywhere to notice. That blocks any second writer: it is why the
// two-point fill cannot journal, and it is the same territory as the carve-undo
// defect that shipped (item 4).
//
// The fix is the one that already worked for faces: name the thing by the dot of
// the op that created it. `id` stays the LOCAL handle — it is what the hot loops
// (is_carved, pockets, csg4_view, fingerprint) index by, and it must stay an
// integer — and `key` becomes the DURABLE name. The store owns the mapping.
//
// Imported brushes need no allocation at all: every peer derives them from the
// same .MAP in the same deterministic order, so "i:<level_id>:<n>" is agreed
// without any replica having been involved. Exactly the reserved-"i:" convention
// plane_key already uses.
//
// IDEMPOTENCE IS NOT OPTIONAL. Replay is not once-only — state_changed fires on
// bulk load AND on every live update, including a peer's own writes coming back
// confirmed — so add_brush must be idempotent on a key it already holds, or the
// level grows a duplicate brush per round trip. This is the same reasoning
// add_face already carries.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/bident \
//          tests/test_brush_identity.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_csg4.h"
#include <cstdio>
#include <string>
#include <vector>
static int fails=0;
static void ok(const char*w,bool c){ if(!c)fails++; printf("  %-58s %s\n",w,c?"OK":"**FAIL**"); }
static void eqi(const char*w,long g,long v){ bool c=g==v; if(!c)fails++; printf("  %-58s %-6ld want %-6ld %s\n",w,g,v,c?"OK":"**FAIL**"); }
static void eqs(const char*w,const std::string&g,const std::string&v){ bool c=g==v; if(!c)fails++; printf("  %-58s %-14s want %-14s %s\n",w,g.c_str(),v.c_str(),c?"OK":"**FAIL**"); }

static std::vector<ConvexPlane> box(float a,float b,float c,float d,float e,float f){
  return {{-1,0,0,-a},{1,0,0,d},{0,-1,0,-b},{0,1,0,e},{0,0,-1,-c},{0,0,1,f}};
}
static const std::vector<uint32_t> P6{0,0,0,0,0,0};

// An "import": the same brushes in the same order, the way two peers reading the
// same .MAP produce them.
static void import_two_brushes(DCBrushStore&s,uint32_t level_id){
  s.add_brush(box(0,0,0,1,1,1),P6,3,level_id);
  s.add_brush(box(2,0,0,3,1,1),P6,4,level_id);
}

int main(){
  printf("=== Brush identity across peers ===\n\n");

  printf("-- imported brushes are named deterministically, no allocation --\n");
  DCBrushStore a,b;
  import_two_brushes(a,0);
  import_two_brushes(b,0);
  eqs("peer A brush 0 key",a.get(1)->key,"i:0:0");
  eqs("peer B brush 0 key",b.get(1)->key,"i:0:0");
  eqs("peer A brush 1 key",a.get(2)->key,"i:0:1");
  ok ("both peers agree on every imported key",
      a.get(1)->key==b.get(1)->key && a.get(2)->key==b.get(2)->key);
  // A second level in the same store must not reuse the first level's names. On
  // its OWN store — a and b are a matched peer pair below, and giving one of them
  // extra brushes would put their counters in different places, which is exactly
  // the condition the control needs to NOT have.
  DCBrushStore two_levels;
  import_two_brushes(two_levels,0);
  import_two_brushes(two_levels,7);
  eqs("a second level gets its own namespace",two_levels.get(3)->key,"i:7:0");
  ok ("and does not collide with level 0",two_levels.get(3)->key!=two_levels.get(1)->key);

  printf("\n-- CONTROL: the local id is ambiguous across peers, the key is not --\n");
  // This is the defect. Two peers holding the SAME map — so their counters sit at
  // the same place, which is the ordinary case and not a contrived one — each
  // create one brush (a fill, a new block). Their LOCAL ids come out identical;
  // the geometry does not. An op-log keyed by id moves the wrong brush, silently.
  const uint32_t ia=a.add_brush(box(10,0,0,11,1,1),P6,5,0,false,"alice:41");
  const uint32_t ib=b.add_brush(box(20,0,0,21,1,1),P6,6,0,false,"bob:7");
  eqi("peer A's created brush takes local id",(long)ia,3);
  eqi("peer B's created brush takes THE SAME local id",(long)ib,3);
  ok ("...so identity-by-id is ambiguous (this is the bug)",ia==ib);
  ok ("identity-by-key is not",a.get(ia)->key!=b.get(ib)->key);
  eqs("and each carries the dot of the op that made it",a.get(ia)->key,"alice:41");

  printf("\n-- replay is idempotent on a key the store already holds --\n");
  const int before=(int)a.brushes().size();
  const uint32_t again=a.add_brush(box(10,0,0,11,1,1),P6,5,0,false,"alice:41");
  eqi("re-adding the same dot returns the SAME id",(long)again,(long)ia);
  eqi("and adds no brush",(long)a.brushes().size(),before);
  // Geometry is NOT re-written either: the record that created a brush is the
  // authority for its shape only at creation; later ops carry their own records.
  const uint32_t moved=a.add_brush(box(99,0,0,100,1,1),P6,5,0,false,"alice:41");
  eqi("a duplicate dot does not silently reshape the brush",(long)moved,(long)ia);
  ok ("...the original plane set is intact",a.get(ia)->planes[1].d==11.0f);

  printf("\n-- resolving a durable name to the local handle --\n");
  eqi("brush_by_key finds a created brush",(long)a.brush_by_key("alice:41"),(long)ia);
  eqi("brush_by_key finds an imported brush",(long)a.brush_by_key("i:0:1"),2);
  eqi("an unknown key resolves to none",(long)a.brush_by_key("nobody:1"),0);
  eqi("an empty key resolves to none",(long)a.brush_by_key(""),0);

  printf("\n-- the name outlives a tombstone, because the id is never reused --\n");
  ok ("tombstone",a.remove_brush(ia));
  eqi("the key still resolves while tombstoned",(long)a.brush_by_key("alice:41"),(long)ia);
  ok ("restore",a.restore_brush(ia));
  eqi("and resolves to the same handle after restore",(long)a.brush_by_key("alice:41"),(long)ia);

  printf("\n-- a plane set that encloses nothing is refused at creation --\n");
  // set_face_plane and add_face both validate; add_brush did not, so the fill had
  // to create a brush, rebuild, count live faces and tombstone it after the fact.
  // A journalled create must not be able to mint invisible geometry.
  const int n_before=(int)a.brushes().size();
  // Opposed half-spaces with no common interior: x >= 5 AND x <= 1.
  std::vector<ConvexPlane> empty{{-1,0,0,-5},{1,0,0,1},{0,-1,0,0},{0,1,0,1},{0,0,-1,0},{0,0,1,1}};
  eqi("add_brush refuses it",(long)a.add_brush(empty,P6,0,0,false,"alice:42"),0);
  eqi("and stores nothing",(long)a.brushes().size(),n_before);
  eqi("so its dot is unknown",(long)a.brush_by_key("alice:42"),0);

  printf("\n%s\n",fails?"FAILED":"PASS");
  return fails?1:0;
}