Sistem generasi prosedural pada arsitektur client-server menuntut satu jaminan utama: parameter input identik harus menghasilkan output data spasial yang identik (deterministik). Pada praktiknya, implementasi API prosedural berbasis gradient noise (seperti Perlin atau Simplex noise) sering mengalami drift akibat representasi floating point antar-arsitektur mesin, JSON precision loss pada integer 64-bit, dan latensi payload transfer matriks dimensi besar.

Tantangan Integritas Kontrak API Prosedural

Membangun endpoint yang mengekspos fungsi matematis seperti f(x, y, seed) -> float memiliki karakteristik berbeda dibanding API CRUD konvensional:

  • Precision Loss Seed 64-bit: Angka acak seed integer 64-bit (unsigned/signed) melampaui batas representasi JavaScript Number.MAX_SAFE_INTEGER (253 - 1). Parsing otomatis JSON payload pada client browser akan membulatkan bit terbawah dan merusak output determinisme. Solusi: Representasikan seed sebagai string desimal pada payload API dan parse ke uint64 di sisi backend runtime.
  • Ketidakpastian IEEE 754: Perbedaan instruksi CPU (misal dukungan FMA vs SSE non-fused), compiler optimization flags, atau library runtime math antar platform (Node.js vs Go vs WASM client) dapat menghasilkan deviasi pecahan desimal pada precision 32-bit/64-bit.
  • Validasi Boundary Input: Koordinat spasial yang terlalu besar dapat menyebabkan degradasi presisi akibat pembulatan floating point (floating point cancellation) pada operasi perkalian frekuensi noise. Parameter step dan scale harus memiliki batas toleransi minimal yang ketat.

Mitigasi Drift IEEE 754 Antar-Runtime

Jika client (misalnya engine visualisasi 3D WebGL) dan backend sama-sama menghitung noise secara terdistribusi, perbedaan implementasi fungsi trigonometri (sin, cos) atau interpolasi polinomial Hermite dapat menghasilkan desinkronisasi. Hindari implementasi noise yang bergantung pada fungsi trigonometri bawaan bahasa pemrograman untuk menghasilkan gradien acak.

Gunakan pendekatan lookup table berbasis permutasi terenkapsulasi atau hash integer murni (seperti MurmurHash3 atau PCG Hash) yang dipetakan langsung ke vektor gradien diskret. Semua perhitungan interpolasi float wajib menggunakan formulasi eksplisit:

// Linear interpolation eksplisit untuk menghindari ketidakpastian FMA
func Lerp(a, b, t float64) float64 {
    return a + t*(b-a)
}

Desain Kontrak Endpoint dan Validasi

Contoh implementasi production-ready backend menggunakan Go (net/http). Parameter divalidasi secara eksplisit, hash deterministik dihitung untuk header ETag HTTP caching, dan output diekspos sebagai stream byte biner (Float32Array) untuk memangkas overhead ukuran payload JSON.

package main

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"math"
	"net/http"
	"strconv"
)

type TerrainParams struct {
	Seed   uint64
	Width  int
	Height int
	Scale  float64
}

func parseAndValidate(r *http.Request) (TerrainParams, error) {
	q := r.URL.Query()
	seedStr := q.Get("seed")
	seed, err := strconv.ParseUint(seedStr, 10, 64)
	if err != nil {
		return TerrainParams{}, fmt.Errorf("invalid uint64 seed: %w", err)
	}

	width, err := strconv.Atoi(q.Get("width"))
	if err != nil || width <= 0 || width > 2048 {
		return TerrainParams{}, fmt.Errorf("width must be between 1 and 2048")
	}

	height, err := strconv.Atoi(q.Get("height"))
	if err != nil || height <= 0 || height > 2048 {
		return TerrainParams{}, fmt.Errorf("height must be between 1 and 2048")
	}

	scale, err := strconv.ParseFloat(q.Get("scale"), 64)
	if err != nil || scale < 0.0001 || scale > 1.0 || math.IsNaN(scale) {
		return TerrainParams{}, fmt.Errorf("scale must be between 0.0001 and 1.0")
	}

	return TerrainParams{Seed: seed, Width: width, Height: height, Scale: scale}, nil
}

func computeETag(p TerrainParams) string {
	h := sha256.New()
	fmt.Fprintf(h, "%d:%d:%d:%.8f", p.Seed, p.Width, p.Height, p.Scale)
	return `"` + hex.EncodeToString(h.Sum(nil)) + `"`
}

// Dummy simple value noise untuk demonstrasi determinisme matematis
func sampleNoise(x, y float64, seed uint64) float32 {
	// Simpan state hash berbasis integer untuk menghindari drift math bawaan
	n := int64(x*374761393) ^ int64(y*668265263) ^ int64(seed)
	n = (n ^ (n >> 13)) * 1274126177
	return float32(float64(n&0x7FFFFFFF) / float64(0x7FFFFFFF))
}

func HandleTerrain(w http.ResponseWriter, r *http.Request) {
	params, err := parseAndValidate(r)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	etag := computeETag(params)
	if match := r.Header.Get("If-None-Match"); match == etag {
		w.WriteHeader(http.StatusNotModified)
		return
	}

	w.Header().Set("ETag", etag)
	w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	w.Header().Set("Content-Type", "application/octet-stream")

	flusher, ok := w.(http.Flusher)
	buf := make([]byte, 4)

	for y := 0; y < params.Height; y++ {
		for x := 0; x < params.Width; x++ {
			val := sampleNoise(float64(x)*params.Scale, float64(y)*params.Scale, params.Seed)
			binary.LittleEndian.PutUint32(buf, math.Float32bits(val))
			if _, writeErr := w.Write(buf); writeErr != nil {
				return
			}
		}
		if ok && y%64 == 0 {
			flusher.Flush()
		}
	}
}

Konsumsi Stream Data pada TypeScript Client

Di sisi client (Web Browser/Node.js), konsumsi response biner application/octet-stream langsung ke dalam Float32Array buffer tanpa parsing JSON untuk menjaga performa memori dan presisi:

async function fetchProceduralChunk(url: string, seed: bigint, width: number, height: number, scale: number): Promise<Float32Array> {
  const params = new URLSearchParams({
    seed: seed.toString(), // Wajib string agar tidak overflow
    width: width.toString(),
    height: height.toString(),
    scale: scale.toFixed(6)
  });

  const response = await fetch(`${url}?${params}`);
  if (!response.ok) {
    throw new Error(`Generation failed: ${response.statusText}`);
  }

  const arrayBuffer = await response.arrayBuffer();
  // Mengakses stream mentah langsung sebagai representasi Float32 IEEE 754 Little-Endian
  return new Float32Array(arrayBuffer);
}

Optimasi Cache dan Kompresi

Karena komputasi prosedural memiliki sifat fungsional murni di mana Input -> Output bernilai konstan abadi, header HTTP harus memanfaatkan instruksi caching deterministik:

  • ETag Berbasis Canonical Hash: Hash SHA-256 dari seluruh parameter yang diurutkan secara kanonikal memastikan proxy server atau browser cache membatalkan download jika parameter diubah sebesar 0.000001 sekalipun.
  • Cache-Control: Gunakan direktif public, max-age=31536000, immutable. Respons tidak boleh kadaluwarsa karena komputasi noise untuk seed tertentu tidak pernah berubah sepanjang waktu.
  • Binary Stream vs JSON: Mengirimkan array 2D berukuran 512x512 via JSON berformat float desimal memakan ~2-3 MB teks. Mengirimkannya via format raw float32 biner berukuran tepat 512 * 512 * 4 bytes = 1.048.576 bytes (~1 MB uncompressed, turun drastis di bawah transfer Brotli/Gzip).