惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
博客园 - 【当耐特】
Cloudbric
Cloudbric
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Attack and Defense Labs
Attack and Defense Labs
爱范儿
爱范儿
The Cloudflare Blog
腾讯CDC
Security Archives - TechRepublic
Security Archives - TechRepublic
TaoSecurity Blog
TaoSecurity Blog
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
C
Check Point Blog
Schneier on Security
Schneier on Security
S
Schneier on Security
J
Java Code Geeks
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
About on SuperTechFans
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
A
Arctic Wolf
S
Secure Thoughts
P
Palo Alto Networks Blog
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 三生石上(FineUI控件)
D
Darknet – Hacking Tools, Hacker News & Cyber Security
量子位
U
Unit 42
I
InfoQ
D
DataBreaches.Net
P
Privacy International News Feed
T
Troy Hunt's Blog
博客园 - 叶小钗
T
Threatpost
博客园 - Franky
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
IT之家
IT之家
www.infosecurity-magazine.com
www.infosecurity-magazine.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cisco Blogs

Hacker News

Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests Retrofitting JIT Compilers into C Interpreters IPv6 – Google The Accursèd Alphabetical Clock Cybersecurity Looks Like Proof of Work Now Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent When moving fast, talking is the first thing to break Too much Discussion of the XOR swap trick – Heather Cafe Introduction to Spherical Harmonics for Graphics Programmers The Grand Line Building a Z-Machine in the worst possible language High-Level Rust: Getting 80% of the Benefits with 20% of the Pain GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The air throughout our homes is infused with microplastics. But there are things you can do to breathe less of them The disturbing white paper Red Hat is trying to erase from the internet – OSnews The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong Artemis II crew splashes down near San Diego after historic moon mission We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Cooperative Vectors Introduction | Evolve Keeping a Postgres queue healthy — PlanetScale Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? The Zettelkasten Method in Obsidian: A Practical Setup Guide Artemis II Is Competency Porn and We Are Starving For It WeakC4 Flight Viz — Cockpit View A Mexican surveillance giant you’ve never heard of is now watching the U.S. border Surelock: Deadlock-Free Mutexes for Rust RISC-V 101 – what is it and what does it mean for Canonical? | Ubuntu The Problem That Built an Industry How Much Linear Memory Access Is Enough? | Solidean Investigating Split Locks on x86-64 Simplest hash functions Sybilproof reputation mechanisms (2005) [pdf] What is a property? How Complex is my Code? Static code analysis in Kotlin — tools overview Toffoli gates are all you need PGLite evangelism dcmake: a new CMake debugger UI Clojure on Fennel part one: Persistent Data Structures Fragments: April 2 Python Release Python install manager 26.1 The Life and Death of the Book Review - Liberties Introducing Database Traffic Control — PlanetScale Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Building slogbox Apple Silicon and Virtual Machines: Beating the 2 VM Limit Who was “Not Even Wrong” first? Pokemon Evolution Vs Darwinian Evolution The APL Programming Language Source Code
Floats Don't Agree With Themselves
2026-05-06 · via Hacker News

I was debugging a polygon-overlap test that worked locally and failed on the server. Same code. Same input. Different answer.

The function deciding the answer was small. Three points A, B, C; return the sign of (B - A) cross (C - A). That sign tells you whether a vertex is convex or reflex, whether a diagonal stays inside the polygon, whether a point is above a line or below. On x86, LLVM had folded the multiply and the subtract into a single fma — one rounding step instead of two. On WASM there is no FMA, so two roundings. A vertex sitting in the epsilon neighborhood of zero fell on opposite sides. From one bit of disagreement, the whole decomposition forked.

This wasn't a bug in my code. It was IEEE 754 working as advertised. The standard pins down the storage format. It does not pin down behavior. Reassociation, FMA contraction, x87 registers at 80 bits, denormal flush flags — four separate ways to get a different answer from the same inputs. Tightening tolerances doesn't help: convex decomposition is discrete. A vertex is reflex or it isn't. An epsilon at the input is a binary difference at the output.

So I wrote exact-poly, a 2D geometry library that uses no floats at all.

A 10-vertex star polygon decomposed into 5 convex parts, with vertex labels v0–v9 and part labels p0–p5.

A 10-vertex star decomposed into 5 convex parts. Vertex labels (v0v9) match input order; part labels (p0.0p5.3) are emitted by the cascade. Sum of part areas equals the ring area, exactly.

Try it

Why floats aren't the same everywhere

IEEE 754 specifies formats and basic operations. The gaps between those operations are where reproducibility leaks out.

EffectMechanismWhere it diverges
Intermediate registersx87 FPU keeps values in 80 bits, rounds only on a spill to memoryx86 vs ARM, RISC-V, WASM (no extended precision)
Fused multiply-addfma(a, b, c) rounds once instead of twice — more accurate, different resultLLVM enables on one target, disables on the next
Reassociation(a + b) + c rewritten as a + (b + c) changes rounding-ffast-math default, often on without intent
DenormalsFlush-to-zero avoids the subnormal slowdownPer-process flag, silently flipped

"IEEE 754 compatibility" is about how floats are stored, not how they behave. Reproducibility has to be asked for explicitly: one architecture, one toolchain, one set of flags. Cross a process or ISA boundary and the guarantees stop.

Why one bit ruins a decomposition

cross_sign(A, B, C) is the only signal the algorithm has. Positive means left turn, negative right, zero collinear. From that sign comes reflex vs convex, inside vs outside, above vs below.

Near zero — three points nearly collinear — float can return +1e-12 on one machine and -2e-13 on another. Flip that one sign bit and a different vertex is reflex. A different reflex vertex means a different starting cut, which means a different decomposition graph. One bit, dominoes.

"Round to a micrometer before comparing" doesn't save you. Convex decomposition is discrete: a vertex is reflex or it isn't. Any epsilon zone around zero eventually catches a real polygon, and the behavior forks.

The honest fix is not to use floats. With integers the sign is bits, not an approximation. Same input, same answer on x86, ARM, WASM — anywhere i64 exists, and on platforms with no float type at all. Shewchuk's 1996 paper on adaptive precision predicates is what Triangle and CGAL sit on; everyone is patching the same hole. exact-poly takes the most direct route: compute the cross product in i128 with no filter. One extra multiplier per call, no "try double, expand on failure." Cheap and correct, always.

Picking a scale

"Don't use floats" is simple with an expensive corollary: integer arithmetic doesn't scale itself. You pick a scale for your problem and pay for the choice.

The budget is i64::MAX ≈ 9.2 × 10^18. Two knobs: unit of measure, and the largest coordinate you ever want to handle. Their product hits the ceiling.

DomainSCALE1 unitMax coord (i64)
Geodesy (Web Mercator)10⁶1 µm±9 × 10¹² m — more than the planet
Physics engine10³1 mm±9 × 10¹⁵ m — solar-system territory
CAD machining10⁹1 nm±9 × 10⁹ m — factory floor, not a city

There is no right scale. Each is a tradeoff.

exact-poly is currently calibrated for geodesy. SCALE = 1_000_000. One meter is a million units; one unit is a micrometer. Consumer GPS is good to about a centimeter on a good day, so a micrometer is four orders finer than anything we measure — not for the user, but as headroom for edge normalization, midpoints, and snapping.

Earth's circumference in Web Mercator is 40_075_017 meters, or 4 × 10^13 units. That's the upper bound of the use case, not the library. From there to i64::MAX is five and a half orders of headroom. Every coordinate on the planet fits in i64.

SCALE and MAX_WORLD are hardcoded at compile time on purpose. Change them at runtime and two processes running the same code can quietly disagree about what a "meter" is — exactly the failure the library was built to prevent. Forking and recompiling for a different calibration is two minutes of work; runtime configurability is a foot-gun.

When i128 shows up

The cross product is (bx - ax)(cy - ay) - (by - ay)(cx - ax). Differences run up to ~8 × 10^13; their product can hit 6.4 × 10^27 — well past i64.

i128::MAX is about 1.7 × 10^38, ten orders of headroom. The rule is one line: coordinates are i64, anything multiplied is i128.

// src/signed.rs:21-27
pub fn cross_sign(ax: i64, ay: i64, bx: i64, by: i64, cx: i64, cy: i64) -> i128 {
let dx1 = (bx as i128) - (ax as i128);
let dy1 = (by as i128) - (ay as i128);
let dx2 = (cx as i128) - (ax as i128);
let dy2 = (cy as i128) - (ay as i128);
dx1 * dy2 - dy1 * dx2
}

Widen before subtract

(bx as i128) - (ax as i128), not (bx - ax) as i128. Subtract first in i64 and the multiply overflows; you get garbage in i128 shape. Half the naive integer-geometry implementations I've read get this backwards.

Why i128 and not arbitrary-precision rationals like GMP? Because i128 fits in two registers, runs in hardware, allocates nothing. Rationals want a heap and branches. For a cross product the precision needed is bounded: i64 in, product of two differences out, fits in i128. Not infinite, just enough for a proven bound. Same trick Shewchuk uses in filtered predicates: cheap-and-correct first, fall back only if you must. [1]

Area as exact equality

The shoelace formula gives 2 × area. No reason to divide by 2 — every check works on the doubled value, and dividing throws away the low bit. Why throw away a bit you paid for in i128?

twice_area is a signed i128. Positive for CCW, negative for CW. The central invariant of the library is this:

sum(twice_area(parts[i])) == twice_area(original_ring)

==, not abs(a - b) < eps. Exact integer equality.

On floats this invariant is impossible in principle — accumulating N areas perturbs low bits in different ways. On integers the parts add up to the whole, bit for bit. If a single bit is off, the decomposer lost or duplicated a microscopic piece, and the test fails.

Why decompose at all

The Separating Axis Theorem is a fast, simple test for whether two polygons overlap. It only works on convex polygons. Anything non-convex has to be carved into convex parts and checked pairwise.

The simplest non-convex shape is an L. One reflex vertex and SAT no longer applies. A typical territory polygon — a parcel, a frequency band, a sensor footprint — has thirty to fifty vertices and several reflex corners. SAT needs convex parts before it can answer "do these overlap" honestly.

A cascade with rotations

Algorithms for convex decomposition have been around since the eighties. Chazelle published the asymptotically optimal O(n + r²) in 1991, and that ceiling still stands. Implementing it honestly in integer arithmetic is three months of work and an unstable trapezoidal decomposition to keep upright. For practical n < 100, simpler algorithms run faster than Chazelle's implementation runs at all. Textbooks have Chazelle 1991. Production has Bayazit, Keil, and Hertel-Mehlhorn.

Each of the simpler algorithms has a weak spot. ExactPartition is a greedy partitioner that prioritizes diagonals from reflex vertices — fast, usually works, gives up on pathological inputs. Bayazit is recursive and can insert Steiner points mid-edge — stronger, but doesn't always converge in a reasonable number of steps. EarClip is the 1985 ear-clipping triangulation; it works on any simple polygon and produces O(n) triangles, which then need merging. That's where Hertel-Mehlhorn (1983) comes in — a merge step with a 4 × OPT guarantee on part count.

The trick is not to pick one. It's to chain them. If the first fails, the next picks up. If all three fail, rotate the ring and try again. Defense in depth.

Figure 1. The cascade. Each algorithm tries; on failure the next picks up. If all three fail, rotate the ring and retry.

The Hertel-Mehlhorn merge step is itself a small example of how cheap integers make things. To merge two triangles across a shared edge, you check that the result stays convex: two cross products with the same sign. No epsilon, no tolerance.

The rotation step sounds like "try again and pray." It is a heuristic — no optimality proof. It works because pathology is usually a function of one vertex, not all of them. The starting index is a lever you can pull without changing the geometry: same area, same perimeter, same set of reflex vertices. Different traversal, different result. Worst case is ; in practice, almost everything is solved at rotation zero.

Three traps in one seam

Bayazit sometimes inserts a vertex mid-edge — a Steiner point — when no diagonal between existing vertices works. Useful, but it lights up three integer-specific traps.

Midpoint without losing a bit

(a + b) / 2 on integers throws away the low bit; the point ends up a half-unit off, and collinearity with neighbors breaks. The fix is round_div2(a + b) >> 1 with round-half-away-from-zero for negatives.

Snap away from endpoints

If a midpoint lands close to an existing vertex, it has to be nudged off, or three points go collinear and break in_cone for every diagonal through them.

Synthetic-vertex bookkeeping

Steiner points are tagged so they don't drift the input vertex count. They live internally during decomposition and materialize into the outer ring only at commit. Otherwise a downstream validator counts a different number of vertices than the client created, and the system silently disagrees with itself again — exactly the failure mode the integer rewrite was supposed to eliminate.

These aren't brilliant ideas. They're three small wedges driven into the same seam, where continuous geometry meets discrete arithmetic. The literature mostly skips them. They appear when a test on a specific polygon refuses to converge.

What doesn't work

The honest list, not the marketing one.

  • Self-intersecting input — rejected by is_simple() (O(n²) over non-adjacent edge pairs). A figure-8 returns DecompError::NotSimple. No heroic recovery; fix it on the caller side, e.g. with a Clipper2 boolean preprocess.
  • Polygons with holes — not supported. Outer ring only.
  • Parts above MAX_VERTICES_PER_PART — validator rejects. Not a fallback; a signal that the input doesn't fit the configured limits.
  • Zero-area or collinear three-vertex polygons — rejected after normalize_ring collapses consecutive collinear vertices.
  • Benchmarks for n > 1000 — not published. For n < 100 the cascade runs in milliseconds (subjective — I haven't run criterion properly). At n = 1000+, rotations × is_simple × algorithm cost can hit seconds.

What the library doesn't try to solve, it rejects on input. No undefined behavior, no infinite loops on figure-8s.

Back to determinism

The cascade is a tree of conditional branches. Every branch reduces to the sign of a cross product, a comparison of i128 areas, or a comparison of i64 lengths. No floats anywhere.

The same ring produces the same decomposition on every architecture: in the browser (wasm32), on the server (x86_64), and in runtimes with no float type at all — a Cortex-M0 with no FPU, a ZK circuit IR, a Move VM. One Rust crate, many compilation targets, bit-for-bit identical results everywhere. [2]

Figure 2. One source crate, five compilation targets, identical i64/i128 output everywhere.

Tests as invariants

The repository's tests are organized around properties, not coverage. The global one is area conservation: after any successful decomposition, the sum of part areas equals the original ring area, exactly. One assertion catches almost any local mistake.

let original = twice_area_fp2(&ring);
let result = decompose(&ring, algo, cfg)?;
let sum: i128 = result.parts.iter()
.map(|p| twice_area_fp2(p))
.sum();
assert_eq!(sum, original);

Regression tests carry their history:

TestProperty guarded
l_shapecanonical reflex case — if it breaks, everything breaks
comb_twelve_verticestwelve-vertex comb earlier versions misclassified
sat_touching_edges_do_not_overlapstrict > overlap, not >= — squares sharing an edge are touching, not overlapping
cross_sign_no_overflow_with_max_world_coordinatescross product at 4 × 10¹³ lands at 1.6 × 10²⁷, well inside i128

The last one exists so the next person who wonders "can we drop to i64?" sees a failing test.

validate_onchain.rs mirrors the validator's checks: limits on parts, vertices, minimum edge length, compactness. Same logic, same step order. If decompose and validate_onchain pass locally, they pass remotely. Not "probably." Always — the code is one binary on both sides.

Closing

Floats are about precision. Integer arithmetic is about agreement. When two processes run the same algorithm and have to arrive at the same answer, the precision of one process doesn't help. cross_sign on floats isn't lying — it just rounds in its own direction. On two machines, the directions don't have to match. On integers there is only one direction.

None of this is unique to any one application. Lockstep game simulation. A geofence checked by three microservices on three toolchains. ZK witness generation. Embedded code with no FPU. Reproducible ML inference. Every one is the same hole and wants the same fix. The algorithms are old — Hertel-Mehlhorn 1983, ear-clipping 1985, Chazelle 1991, Bayazit 2002. The thinking has been done. What's left is to rewrite the implementations without floats and put them under i128.

exact-poly does that for convex decomposition and the SAT overlap check. Boolean operations, offsetting, Minkowski sums — same recipe, still to be written. PRs welcome.


Footnotes

[1] The Shewchuk paper is worth reading even if you never plan to ship a CGAL kernel. The trick is that you can run the cheap path almost always and fall back to expensive arbitrary precision only when the cheap path can't certify its answer. For a fixed-degree expression like a 2D cross product on bounded inputs, the bound is known in advance and the fallback never fires — which is the case exact-poly exploits.

[2] The "no floats anywhere" claim is enforced by the toolchain in addition to the code: there are no f32 or f64 types in the public API or the internal pipeline. WASM compiled from this Rust crate doesn't import any float instructions either, which means the same module loads in environments where float instructions are forbidden — ZK circuit IRs and certain VMs among them.