diff --git a/CMakeLists.txt b/CMakeLists.txt
index e503343..9a52154 100755
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -38,7 +38,9 @@ set(SOURCES
FileVersion.cpp
FindCmd.cpp
GeneralDataEnry.cpp
+ gp2ccline.cpp
gp2exe.cpp
+ gp2geom.cpp
GPPitDraw.cpp
GPTrack.cpp
GPTrackDraw.cpp
diff --git a/GPTrack.cpp b/GPTrack.cpp
old mode 100755
new mode 100644
index 9456e2b..972be67
--- a/GPTrack.cpp
+++ b/GPTrack.cpp
@@ -11,6 +11,9 @@
#include "TrackSection.h"
#include "TrackObjectDefinition.h"
#include "CCLineSection.h"
+#include "gp2cc.h" // shared compiled-track data structs (gp2cc_track / gp2cc_ccgeo)
+#include "gp2geom.hpp" // semantic C++ track-geometry compiler + the gv() cosine helper
+#include "gp2ccline.hpp" // C++ cc-line (racing-line) solver on the formula tables
#include "TrackCmd.h"
#include "InternalObject.h"
#include "JamFileEditor.h"
@@ -144,6 +147,8 @@ void GPTrack::Create()
PROFILE(showPitLane, TRUE)
PROFILE(showObjects, FALSE)
PROFILE(showCCLine, TRUE)
+ PROFILE(showComputed, FALSE)
+ PROFILE(showComputedCCLine, FALSE)
PROFILE(showHiddenAsGray, TRUE)
PROFILE(showTrackPie, FALSE)
@@ -412,6 +417,8 @@ GPTrack::~GPTrack()
WR_PROFILE(showPitLane)
WR_PROFILE(showObjects)
WR_PROFILE(showCCLine)
+ WR_PROFILE(showComputed)
+ WR_PROFILE(showComputedCCLine)
WR_PROFILE(showHiddenAsGray)
WR_PROFILE(showTrackPie)
@@ -4030,6 +4037,173 @@ void GPTrack::drawCCLine(Display *g)
}
}
+// ---------------------------------------------------------------------------
+// Compiled overlays (bit-exact, gp2geom) drawn ON TOP of the normal view.
+// buildCompiledOverlay() rebuilds the .dat from the live track (RecreateData),
+// runs the gp2geom geometry compiler (and the cc-line only if that overlay is
+// on), and projects every segment into the editor's frame. The two draw
+// functions then render the compiled road (edges + section dividers, BLUE)
+// and the compiled racing line (line + sector ticks, ORANGE; selected sector
+// bright YELLOW), each toggled independently.
+//
+// Coordinate map: gp2geom positions are 1/8-world-units (X8/Y8); GP2 steps 128
+// world-units (=1024 X8) per segment while the editor uses 1 unit/segment, so
+// scale = 1/128, anchored at track section 0's start, same heading orientation.
+// Each point = centre + perpendicular(heading) * (offset / 8 world units).
+// ---------------------------------------------------------------------------
+namespace {
+ struct CompiledOverlay {
+ bool valid;
+ bool hasCC;
+ int n;
+ gp2cc_track gt; // compiled geometry
+ short bl[GP2CC_MAXSEG], a18[GP2CC_MAXSEG]; // cc-line bestLine / angle18
+ int cmdSeg[GP2CC_MAXSEG], numCmds; // first segment of each cc-command
+ double lex[GP2CC_MAXSEG], ley[GP2CC_MAXSEG]; // left edge (editor coords)
+ double rex[GP2CC_MAXSEG], rey[GP2CC_MAXSEG]; // right edge
+ double ccx[GP2CC_MAXSEG], ccy[GP2CC_MAXSEG]; // racing line
+ };
+ static CompiledOverlay ov; // shared between buildCompiledOverlay + the draw fns
+}
+
+bool GPTrack::buildCompiledOverlay()
+{
+ ov.valid = false; ov.hasCC = false;
+ if (TrackSections == NULL || TrackSections->size() == 0) return false;
+
+ RecreateData(); // live track -> trackdata[]
+ if (gp2geom::compileGeometry(trackdata, 65535, ov.gt) != 0) return false;
+ int n = ov.gt.n;
+ if (n <= 1) return false;
+ ov.n = n;
+
+ // editor frame: scale 1/128, anchored at section 0's start.
+ TrackSection *s0 = (TrackSection *)TrackSections->elementAt(0);
+ double ox = s0->getStartX(), oy = s0->getStartY();
+ double bwx = ov.gt.X8[0] / 8.0, bwy = ov.gt.Y8[0] / 8.0;
+ const double ES = 1.0 / 128.0;
+
+ for (int i = 0; i < n; i++) {
+ double cosh = gp2geom::gv(ov.gt.angle[i]) / 16384.0;
+ double sinh = gp2geom::gv(0x4000 - ov.gt.angle[i]) / 16384.0;
+ double pvx = -sinh, pvy = cosh; // unit perpendicular (worldX, worldY)
+ double px = ov.gt.X8[i] / 8.0, py = ov.gt.Y8[i] / 8.0;
+ double wl = ov.gt.widthL[i] / 8.0, wr = ov.gt.widthR[i] / 8.0;
+ // edge sign matches the normal view's getLeftSide (a-90 => -pv) / getRightSide
+ // (a+90 => +pv); otherwise widthL/widthR render on the mirror-opposite side.
+ ov.lex[i] = ox + (px - pvx * wl - bwx) * ES; ov.ley[i] = oy + (py - pvy * wl - bwy) * ES;
+ ov.rex[i] = ox + (px + pvx * wr - bwx) * ES; ov.rey[i] = oy + (py + pvy * wr - bwy) * ES;
+ }
+
+ if (showComputedCCLine) {
+ static gp2cc_ccgeo cg;
+ cg.n = n;
+ for (int i = 0; i < n; i++) {
+ cg.segAngle[i] = ov.gt.angle[i];
+ cg.cx8[i] = ov.gt.Y8[i];
+ cg.cy8[i] = ov.gt.X8[i];
+ cg.f14[i] = ov.gt.f14[i];
+ }
+ ov.numCmds = 0;
+ gp2geom::computeCcLine(cg, trackdata, 65535, ov.gt.ccoff, ov.bl, ov.a18, ov.cmdSeg, &ov.numCmds);
+ for (int i = 0; i < n; i++) {
+ double cosh = gp2geom::gv(ov.gt.angle[i]) / 16384.0;
+ double sinh = gp2geom::gv(0x4000 - ov.gt.angle[i]) / 16384.0;
+ double pvx = -sinh, pvy = cosh;
+ double px = ov.gt.X8[i] / 8.0, py = ov.gt.Y8[i] / 8.0;
+ double cc = ov.bl[i] / 8.0;
+ ov.ccx[i] = ox + (px + pvx * cc - bwx) * ES; ov.ccy[i] = oy + (py + pvy * cc - bwy) * ES;
+ }
+ ov.hasCC = true;
+ }
+
+ ov.valid = true;
+ return true;
+}
+
+// Compiled-track overlay: bit-exact road edges + a perpendicular divider at each
+// TrackSection boundary, in BLUE. Numbers / start-finish stay with the normal view.
+void GPTrack::drawCompiledTrack(Display *g)
+{
+ if (!ov.valid) return;
+ int n = ov.n;
+
+ CPen *trackPen = new CPen(PS_SOLID, 1, RGB(0, 96, 255)); // blue
+ g->SelectObject(trackPen);
+
+ for (int i = 0; i < n; i++) { // road edges
+ int j = (i + 1) % n;
+ g->drawLine(ov.lex[i], ov.ley[i], ov.lex[j], ov.ley[j]);
+ g->drawLine(ov.rex[i], ov.rey[i], ov.rex[j], ov.rey[j]);
+ }
+
+ // section dividers: cumulative getLength() gives each section's first segment
+ // (skip index==-99 sections that emit no geometry command, as WriteTrack does).
+ int nsec = TrackSections->size();
+ int seg = 0;
+ for (int k = 0; k < nsec; k++) {
+ TrackSection *t = (TrackSection *)TrackSections->elementAt(k);
+ if (t->index == -99) continue;
+ int s = seg;
+ seg += (int)t->getLength();
+ if (s < 0 || s >= n) continue;
+ g->drawLine(ov.lex[s], ov.ley[s], ov.rex[s], ov.rey[s]);
+ }
+
+ g->setColor(0); // deselect before delete
+ delete trackPen;
+}
+
+// Compiled-cc-line overlay: bit-exact racing line + a perpendicular tick at each
+// cc-sector start, in ORANGE; the SELECTED sector over-drawn in bright YELLOW.
+// CCLineSection[c] starts at cmdSeg[c+1] -- cmdSeg[0] is the seed/start command
+// (the editor's CCLineStart header), not a user-editable sector.
+void GPTrack::drawCompiledCcLine(Display *g)
+{
+ if (!ov.valid || !ov.hasCC) return;
+ int n = ov.n;
+
+ CPen *linePen = new CPen(PS_SOLID, 1, RGB(255, 140, 0)); // orange
+ g->SelectObject(linePen);
+ for (int i = 0; i < n; i++) {
+ int j = (i + 1) % n;
+ g->drawLine(ov.ccx[i], ov.ccy[i], ov.ccx[j], ov.ccy[j]);
+ }
+ g->setColor(0);
+ delete linePen;
+
+ int nSec = (CCLineSections != NULL) ? CCLineSections->size() : 0;
+ CPen *tickPen = new CPen(PS_SOLID, 1, RGB(255, 140, 0)); // orange tick
+ CPen *selPen = new CPen(PS_SOLID, 2, RGB(255, 255, 0)); // bright yellow (selected)
+ for (int c = 0; c < nSec; c++) {
+ int ci = c + 1; // +1: skip the seed command
+ if (ci >= ov.numCmds) break;
+ int s = ov.cmdSeg[ci];
+ if (s < 0 || s >= n) continue;
+ CCLineSection *sec = (CCLineSection *)CCLineSections->elementAt(c);
+ BOOL sel = (sec != NULL) && sec->isSelected();
+
+ if (sel) { // highlight the selected sector
+ int len = (int)sec->getLength();
+ g->SelectObject(selPen);
+ for (int k = 0; k < len; k++) {
+ int i = (s + k) % n, j = (i + 1) % n;
+ g->drawLine(ov.ccx[i], ov.ccy[i], ov.ccx[j], ov.ccy[j]);
+ }
+ }
+
+ double cosh = gp2geom::gv(ov.gt.angle[s]) / 16384.0; // tick perpendicular
+ double sinh = gp2geom::gv(0x4000 - ov.gt.angle[s]) / 16384.0;
+ double tlen = 3.0;
+ double tx = -sinh * tlen, ty = cosh * tlen;
+ g->SelectObject(sel ? selPen : tickPen);
+ g->drawLine(ov.ccx[s] - tx, ov.ccy[s] - ty, ov.ccx[s] + tx, ov.ccy[s] + ty);
+ }
+ g->setColor(0); // deselect before delete
+ delete tickPen;
+ delete selPen;
+}
+
void GPTrack::drawPitlane(Display *g)
{
if (TrackSections == NULL || TrackSections->size() == 0) return;
diff --git a/GPTrack.h b/GPTrack.h
old mode 100755
new mode 100644
index 7946d64..5a6d2b3
--- a/GPTrack.h
+++ b/GPTrack.h
@@ -144,6 +144,13 @@ class GPTrack : public CWnd
drawPitlane(Display *g);
void
drawCCLine(Display *g);
+ // Compiled overlays (bit-exact, gp2geom). buildCompiledOverlay() recompiles the
+ // track once per repaint and fills the shared overlay arrays; the two draw
+ // functions render the track (edges + dividers) and the cc-line (racing line +
+ // sector marks) on top of the normal view. Returns false if compile failed.
+ bool buildCompiledOverlay();
+ void drawCompiledTrack(Display *g);
+ void drawCompiledCcLine(Display *g);
void
drawCameras(Display *g);
void
@@ -243,6 +250,8 @@ class GPTrack : public CWnd
BOOL showPitLane;
BOOL showObjects;
BOOL showCCLine;
+ BOOL showComputed; // compiled-track overlay (bit-exact road edges + dividers)
+ BOOL showComputedCCLine; // compiled-cc-line overlay (bit-exact racing line + sector marks)
BOOL showHiddenAsGray;
BOOL showTrackPie;
BOOL showCameras;
diff --git a/TrackEditor.rc b/TrackEditor.rc
index 580f88f..3e2db8f 100755
--- a/TrackEditor.rc
+++ b/TrackEditor.rc
@@ -274,6 +274,9 @@ BEGIN
BUTTON ID_AIRVIEW
BUTTON ID_DRAWAXIS
BUTTON ID_VIEWCAMERAS
+ SEPARATOR
+ BUTTON ID_VIEW_COMPILED
+ BUTTON ID_VIEW_COMPILED_CCLINE
END
IDR_OBJECT TOOLBAR 16, 15
@@ -472,6 +475,8 @@ BEGIN
MENUITEM "Show Track", VIEW_TRACK
MENUITEM "Show Pitlane", VIEW_PITLANE
MENUITEM "Show CC Line", VIEW_CCLINE
+ MENUITEM "Compiled Track Overlay", ID_VIEW_COMPILED
+ MENUITEM "Compiled CC-Line Overlay", ID_VIEW_COMPILED_CCLINE
MENUITEM "Show Objects", VIEW_OBJECTS
MENUITEM "Show Fences", VIEW_WALLS
MENUITEM "Show Pit Fences", ID_SHOW_SHOWPITFENCES
@@ -1746,6 +1751,8 @@ BEGIN
VIEW_PITLANE "Show Pitlane data\nShow Pit lane"
VIEW_TRACK "Show track data\nShow Track"
VIEW_CCLINE "Show Driving Line\nDriving Line"
+ ID_VIEW_COMPILED "Toggle compiled track overlay\nCompiled Track Overlay"
+ ID_VIEW_COMPILED_CCLINE "Toggle compiled CC-line overlay\nCompiled CC-Line Overlay"
VIEW_OBJECTS "Show trackside objects\nShow Objects"
END
diff --git a/TrackEditor.vcxproj b/TrackEditor.vcxproj
index 8096677..8ae3b2e 100755
--- a/TrackEditor.vcxproj
+++ b/TrackEditor.vcxproj
@@ -174,6 +174,19 @@
+
+ stdcpp17
+ NotUsing
+
+ /constexpr:steps4000000 %(AdditionalOptions)
+
+
+ stdcpp17
+ NotUsing
+
+ /constexpr:steps4000000 %(AdditionalOptions)
+
@@ -279,6 +292,11 @@
+
+
+
+
+
diff --git a/TrackEditorScrollView.cpp b/TrackEditorScrollView.cpp
old mode 100755
new mode 100644
index 5812c42..043ef13
--- a/TrackEditorScrollView.cpp
+++ b/TrackEditorScrollView.cpp
@@ -89,6 +89,10 @@ ON_COMMAND(ID_TRACK_TOOL, OnTrackTool)
ON_COMMAND(ID_ZOOMTOOL, OnZoomtool)
ON_UPDATE_COMMAND_UI(VIEW_CCLINE, OnUpdateCcline)
ON_COMMAND(VIEW_CCLINE, OnCcline)
+ON_UPDATE_COMMAND_UI(ID_VIEW_COMPILED, OnUpdateViewCompiled)
+ON_COMMAND(ID_VIEW_COMPILED, OnViewCompiled)
+ON_UPDATE_COMMAND_UI(ID_VIEW_COMPILED_CCLINE, OnUpdateViewCompiledCCLine)
+ON_COMMAND(ID_VIEW_COMPILED_CCLINE, OnViewCompiledCCLine)
ON_UPDATE_COMMAND_UI(ID_VIEW_OBJ_BITMAPS, OnUpdateViewObjBitmaps)
ON_UPDATE_COMMAND_UI(ID_ZOOMTOOL, OnUpdateZoomtool)
ON_UPDATE_COMMAND_UI(ID_POINTER, OnUpdatePointer)
@@ -464,6 +468,14 @@ void TrackEditorScrollView::OnMyDraw(CDC* pDC)
track->drawCameras(display);
track->drawBlackFlags(display);
track->DrawCCDataLog(display);
+ // bit-exact compiled overlays drawn ON TOP of the normal view, each
+ // toggled independently. Compile once, both overlays share the result.
+ if (track->showComputed || track->showComputedCCLine) {
+ if (track->buildCompiledOverlay()) {
+ if (track->showComputed) track->drawCompiledTrack(display);
+ if (track->showComputedCCLine) track->drawCompiledCcLine(display);
+ }
+ }
}
TrackSection* t = track->getTrackSelection();
@@ -1026,6 +1038,32 @@ void TrackEditorScrollView::OnCcline()
OnDrivingLine();
}
+void TrackEditorScrollView::OnUpdateViewCompiled(CCmdUI* pCmdUI)
+{
+ UPDATE_TRACK(showComputed);
+}
+
+void TrackEditorScrollView::OnViewCompiled()
+{
+ CTrackEditorDoc* pDoc = GetDocument();
+ GPTrack* mytrack = pDoc->getTrack();
+ mytrack->showComputed = !mytrack->showComputed;
+ repaint();
+}
+
+void TrackEditorScrollView::OnUpdateViewCompiledCCLine(CCmdUI* pCmdUI)
+{
+ UPDATE_TRACK(showComputedCCLine);
+}
+
+void TrackEditorScrollView::OnViewCompiledCCLine()
+{
+ CTrackEditorDoc* pDoc = GetDocument();
+ GPTrack* mytrack = pDoc->getTrack();
+ mytrack->showComputedCCLine = !mytrack->showComputedCCLine;
+ repaint();
+}
+
void TrackEditorScrollView::OnUpdateViewObjBitmaps(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
diff --git a/TrackEditorScrollView.h b/TrackEditorScrollView.h
old mode 100755
new mode 100644
index f4e3b0f..fba18d1
--- a/TrackEditorScrollView.h
+++ b/TrackEditorScrollView.h
@@ -142,6 +142,14 @@ class TrackEditorScrollView : public CScrollView
OnUpdateCcline(CCmdUI* pCmdUI);
afx_msg void
OnCcline();
+ afx_msg void
+ OnUpdateViewCompiled(CCmdUI* pCmdUI);
+ afx_msg void
+ OnViewCompiled();
+ afx_msg void
+ OnUpdateViewCompiledCCLine(CCmdUI* pCmdUI);
+ afx_msg void
+ OnViewCompiledCCLine();
afx_msg void
OnUpdateViewObjBitmaps(CCmdUI* pCmdUI);
afx_msg void
diff --git a/gp2atan.hpp b/gp2atan.hpp
new file mode 100644
index 0000000..f381b82
--- /dev/null
+++ b/gp2atan.hpp
@@ -0,0 +1,74 @@
+#pragma once
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// GP2 geometry — the arctangent table expressed as its generating formula.
+//
+// GP2's t_ArithTab1 maps a ratio i/2048 (i = 0..2048, so the ratio runs 0..1)
+// to an angle in binary-radian units (65536 per full turn). It is exactly:
+//
+// ATAN1[i] = round( atan(i / 2048) * 65536 / (2*pi) )
+//
+// Unlike the cosine table, this one is CLEAN: every real entry matches the
+// formula with no hand-tweaked exceptions (verified 2049/2049). Index 2049 is
+// unused padding (0). It is also far less rounding-fragile than COS — only a
+// single entry sits within 1e-3 of a .5 boundary, and it rounds correctly.
+//
+// So this is the poster child for replacing an opaque blob with a formula:
+// one line of intent, no override.
+//
+// WARNING: the entries come from compile-time floating-point rounding, so a
+// different compiler or standard library could round an entry that lands on a
+// .5 boundary the other way. This table is far less fragile than the cosine one
+// (only a single entry is anywhere near a boundary, and it rounds correctly),
+// but if the compiled track or cc-line ever start to DRIFT, a rounding
+// disagreement here — or in gp2cos.hpp — is the first thing to suspect:
+// regenerate the table and diff it against GP2's t_ArithTab1 bytes.
+// ---------------------------------------------------------------------------
+
+namespace gp2geom {
+
+inline constexpr int kAtanGrid = 2050; // 0..2048 real, 2049 = padding
+inline constexpr int kAtanReal = 2048; // last real index
+inline constexpr double kPiAtan = 3.14159265358979323846;
+
+// constexpr sqrt (Newton) and atan, since neither is portably constexpr before
+// C++26. atan is range-reduced toward 0 via atan(x)=2*atan(x/(1+sqrt(1+x^2)))
+// then a Taylor series on the small remainder; accurate to ~1e-15 on [0,1].
+constexpr double atanSqrt(double v) {
+ if (v <= 0.0) return 0.0;
+ double g = v;
+ for (int i = 0; i < 80; ++i) g = 0.5 * (g + v / g);
+ return g;
+}
+constexpr double arctan(double x) {
+ int k = 0;
+ while (x > 0.05) { x = x / (1.0 + atanSqrt(1.0 + x * x)); ++k; }
+ double sum = 0.0, t = x, x2 = x * x;
+ for (int n = 0; n < 12; ++n) { sum += t / (2 * n + 1); t *= -x2; }
+ for (int i = 0; i < k; ++i) sum *= 2.0;
+ return sum;
+}
+
+constexpr short roundToShortA(double v) {
+ long r = (v >= 0.0) ? static_cast(v + 0.5) : static_cast(v - 0.5);
+ return static_cast(r);
+}
+
+constexpr std::array makeAtanTable() {
+ std::array t{};
+ for (int i = 0; i <= kAtanReal; ++i)
+ t[i] = roundToShortA(arctan(static_cast(i) / 2048.0)
+ * 65536.0 / (2.0 * kPiAtan));
+ t[2049] = 0; // unused padding, matches GP2
+ return t;
+}
+
+inline constexpr std::array kArctan = makeAtanTable();
+
+// Cheap spot-checks of the two exact endpoints (no embedded reference table).
+static_assert(kArctan[0] == 0, "atan(0)");
+static_assert(kArctan[2048] == 8192, "atan(1) = pi/4 = 0x2000");
+
+} // namespace gp2geom
diff --git a/gp2cc.h b/gp2cc.h
new file mode 100644
index 0000000..eb63fb2
--- /dev/null
+++ b/gp2cc.h
@@ -0,0 +1,52 @@
+/*
+ * gp2cc.h — the shared compiled-track data format.
+ *
+ * These two structs are the bit-exact output of GP2's track-geometry +
+ * cc-line (racing-line) compilation: gp2cc_track is filled by
+ * gp2geom::compileGeometry, and gp2cc_ccgeo feeds gp2ccline::computeCcLine.
+ * The editor's overlay just draws the arrays — it never touches the fixed
+ * point. Field offsets/units reference the original engine layout (tseg+N).
+ */
+#ifndef GP2CC_H
+#define GP2CC_H
+
+#include
+
+#define GP2CC_MAXSEG 4096
+
+typedef struct {
+ int n; /* number of track segments */
+ int32_t fAngle[GP2CC_MAXSEG]; /* d_TrkStrtAngle per seg, Q16 (full precision) */
+ int16_t angle[GP2CC_MAXSEG]; /* heading = fAngle>>16 (tseg+0) */
+ int16_t startAngle[GP2CC_MAXSEG]; /* d_StartAngle>>16 (tseg+0xA, side-vec) */
+ int32_t f14[GP2CC_MAXSEG]; /* tseg+0x14 = (cmd d1 * 0x6488)>>14 (θ' tilt) */
+ int32_t widthL[GP2CC_MAXSEG]; /* tseg+0x60 left road width (w_TrkBeginWidth) */
+ int32_t widthR[GP2CC_MAXSEG]; /* tseg+0x62 right road width (w_TrkBeginWidth2) */
+ /* side vectors (tseg+0x4C/0x4E left, +0x0C/0x0E right): edge = centre ± side/64.
+ * side = ((scos(startAngle)*width)>>17)<<6 components. (sideRX/RY validated == dump xSide/ySide.) */
+ int16_t sideLX[GP2CC_MAXSEG], sideLY[GP2CC_MAXSEG];
+ int16_t sideRX[GP2CC_MAXSEG], sideRY[GP2CC_MAXSEG];
+ /* exact 1/8-world-unit position (the game's w_TrkStrtX/Y, warped) */
+ int32_t X8[GP2CC_MAXSEG]; /* world X (= dump col4) * 8, warped */
+ int32_t Y8[GP2CC_MAXSEG]; /* world Y (= dump col3) * 8, warped */
+ int32_t rawX8[GP2CC_MAXSEG]; /* world X * 8, BEFORE warp (raw integration) */
+ int32_t rawY8[GP2CC_MAXSEG]; /* world Y * 8, BEFORE warp */
+ int32_t gapX8, gapY8; /* raw closure gap (rawEnd - start), 1/8 units */
+ int ccoff; /* file offset of the cc-line command stream */
+ /* header */
+ int16_t hdr_angle, hdr_X, hdr_Y;
+ uint16_t hdr_width, hdr_c5;
+} gp2cc_track;
+
+/* Per-segment geometry the cc-line compiler consumes. The cc-line internal
+ * convention pairs C94BC with tseg+4 (=dump col3 =world Y) and C94C0 with tseg+8
+ * (=dump col4 =world X), so cx8/cy8 follow that pairing (NOT the X8/Y8 naming). */
+typedef struct {
+ int n;
+ int16_t segAngle[GP2CC_MAXSEG]; /* tseg+0 heading */
+ int32_t cx8[GP2CC_MAXSEG]; /* tseg+4 full 1/8-unit (C94BC-paired) */
+ int32_t cy8[GP2CC_MAXSEG]; /* tseg+8 full 1/8-unit (C94C0-paired) */
+ int32_t f14[GP2CC_MAXSEG]; /* tseg+0x14 (width-derived, drives θ' tilt) */
+} gp2cc_ccgeo;
+
+#endif
diff --git a/gp2ccline.cpp b/gp2ccline.cpp
new file mode 100644
index 0000000..c2c831a
--- /dev/null
+++ b/gp2ccline.cpp
@@ -0,0 +1,279 @@
+#include "gp2ccline.hpp"
+#include "gp2cos.hpp" // gp2geom::kCosine — the verified cosine table
+#include "gp2atan.hpp" // gp2geom::kArctan — the verified arctangent table
+#include
+
+namespace gp2geom {
+namespace {
+
+// Short accessors for the two trig tables used throughout the math below.
+inline int COS (int i) { return kCosine[i]; } // cosine, amplitude 0x4000
+inline int ATAN(int i) { return kArctan[i]; } // arctangent, 0x10000 per turn
+
+#define ONE60 (((int64_t)1) << 60)
+
+// Converts a segment's theta-prime tilt back into a binary-radian heading
+// offset: this is round(2^16 / pi), the inverse of the (pi/2)-scaled tilt that
+// the geometry pass stores in f14.
+constexpr int kInvPiQ16 = 20861; // round(65536 / pi)
+
+inline int s16(int v) { v &= 0xFFFF; return (v & 0x8000) ? v - 0x10000 : v; }
+inline int abs16(int v) { v = s16(v); return v < 0 ? -v : v; }
+inline int rdw(const std::uint8_t* d, int o) { return d[o] | (d[o + 1] << 8); }
+
+// Interpolated cosine, amplitude 0x4000 (8-unit grid lookup + linear lerp).
+inline int gv(int ax) {
+ ax = s16(ax);
+ if (ax < 0) ax = (-ax) & 0xFFFF;
+ int frac = ax & 7;
+ int widx = ((ax >> 2) & 0xFFFE) >> 1;
+ int base = COS(widx), nxt = COS(widx + 1);
+ int d = s16(nxt - base);
+ return s16(base + ((d * frac) >> 3));
+}
+
+// 32-bit integer square root: a FIXED 3-iteration 16-bit Newton step. This is
+// deliberately NOT a true floor-sqrt — the fixed iteration count reproduces the
+// engine's exact (slightly-off) result, which the chaotic loop below depends on.
+std::uint32_t sqrt32(std::uint32_t V) {
+ if (V == 0) return 0;
+ int shift = 0;
+ while (V < 0x40000000u) { shift++; V <<= 2; if (V == 0) return 0; }
+ std::uint32_t half = V >> 1;
+ std::uint32_t di = ((V >> 17) + 0x8000) & 0xFFFF;
+ for (int it = 0; it < 3; it++) {
+ std::uint32_t hi = half >> 16, q;
+ if (hi >= di) q = half & 0xFFFF;
+ else q = half / di;
+ di = ((di >> 1) + q) & 0xFFFF;
+ }
+ return (di >> shift) & 0xFFFF;
+}
+
+// 64-bit integer square root: a FIXED 5-iteration Newton step (same caveat as
+// sqrt32 — the iteration count is part of the result, not an approximation).
+std::uint32_t sqrt64(std::uint64_t V) {
+ if (V == 0) return 0;
+ if ((std::uint32_t)(V >> 32) == 0) return sqrt32((std::uint32_t)V);
+ int shift = 0;
+ while ((V >> 32) < 0x40000000ull) { shift++; V <<= 2; }
+ std::uint64_t half = V >> 1;
+ std::uint32_t est = (std::uint32_t)((half >> 32) + 0x80000000ull);
+ for (int it = 0; it < 5; it++) {
+ std::uint32_t q = (std::uint32_t)(half / est);
+ est = (est >> 1) + q;
+ }
+ return est >> shift;
+}
+
+// cos(|angle|) in Q30 (amplitude 0x40000000) from the cosine table + interpolation.
+std::int32_t cos30(int angle) {
+ int a = abs16(angle);
+ int widx = a >> 3, frac = a & 7;
+ int base = COS(widx);
+ int delta = COS(widx + 1) - COS(widx);
+ return ((std::int32_t)base << 16) + ((std::int32_t)(delta * frac) << 13);
+}
+
+// Q30 cosine and sine of an angle. To keep precision, the larger-magnitude of
+// the two is recovered from the smaller via sqrt(1 - x^2) (computed in Q60),
+// and the smaller-magnitude one is read straight from the table.
+void q30_cossin(int angle, std::int32_t* pcos, std::int32_t* psin) {
+ int a = s16(angle);
+ int coarse_sin = COS(abs16(s16(0x4000 - a)) >> 3);
+ int coarse_sin_abs = coarse_sin < 0 ? -coarse_sin : coarse_sin;
+ std::int32_t c30, s30;
+ if (coarse_sin_abs >= 0x2000) {
+ c30 = cos30(a);
+ std::int64_t smag = (std::int64_t)sqrt64((std::uint64_t)(ONE60 - (std::int64_t)c30 * c30));
+ s30 = coarse_sin < 0 ? (std::int32_t)(-smag) : (std::int32_t)smag;
+ } else {
+ s30 = cos30(s16(0x4000 - a));
+ std::int64_t cmag = (std::int64_t)sqrt64((std::uint64_t)(ONE60 - (std::int64_t)s30 * s30));
+ int coarse_cos = COS(abs16(a) >> 3);
+ c30 = coarse_cos < 0 ? (std::int32_t)(-cmag) : (std::int32_t)cmag;
+ }
+ *pcos = c30; *psin = s30;
+}
+
+// atan2(y, x) in binary radians (0x10000 = a full turn), 16-bit arguments.
+int atan2u(int y, int x) {
+ y = s16(y); x = s16(x);
+ int dy = y < 0 ? -y : y;
+ int dx = x < 0 ? -x : x;
+ int signs_differ = (y < 0) != (x < 0);
+ int a;
+ if (dy >= dx) {
+ if (dy == 0) return 0;
+ int idx = (dx << 11) / dy;
+ a = 0x4000 - ATAN(idx);
+ } else {
+ int idx = (dy << 11) / dx;
+ a = ATAN(idx);
+ }
+ if (signs_differ) a = -a;
+ if (x < 0) a += 0x8000;
+ return s16(a);
+}
+
+// Build the world arc-centre point W for the first segment of a command.
+void build_centre(const gp2cc_ccgeo* g, int idx, int P, int H,
+ std::int32_t arg2, int arg1mul4, std::int32_t* pWx, std::int32_t* pWy) {
+ int seg = idx;
+ int f14 = (int)g->f14[seg];
+ int latP = (int)(((std::int32_t)P * f14) >> 15);
+ int ang = s16(g->segAngle[seg]);
+ int cosA = gv(ang);
+ int sinA = gv(s16(0x4000 - ang));
+ std::int32_t RX = (std::int16_t)(((std::int32_t)P * cosA + (std::int32_t)latP * sinA) >> 14);
+ std::int32_t RY = (std::int16_t)(((std::int32_t)latP * cosA - (std::int32_t)P * sinA) >> 14);
+ RX += g->cx8[seg];
+ RY += g->cy8[seg];
+ int sinH = gv(s16(0x4000 - H));
+ int cosH = gv(s16(H));
+ std::int16_t a1m4 = (std::int16_t)arg1mul4;
+ RX += (std::int16_t)(((std::int32_t)sinH * a1m4) >> 14);
+ RY += (std::int16_t)(((std::int32_t)cosH * a1m4) >> 14);
+ std::int32_t Wx = RX, Wy = RY;
+ if (arg2 != 0) {
+ std::int32_t mag = arg2 < 0 ? -arg2 : arg2;
+ int perp = (arg2 < 0) ? s16(H - 0x4000) : s16(H + 0x4000);
+ std::int32_t c32, s32; q30_cossin(perp, &c32, &s32);
+ Wx += (std::int32_t)(((std::int64_t)s32 * mag) >> 30);
+ Wy += (std::int32_t)(((std::int64_t)c32 * mag) >> 30);
+ }
+ *pWx = Wx; *pWy = Wy;
+}
+
+// Reproject the carried world point W onto segment `idx`, producing the lateral
+// offset P (and, on curved commands, the updated heading H).
+void reproject(const gp2cc_ccgeo* g, int idx, std::int32_t Wx, std::int32_t Wy,
+ std::int32_t arg2, int* pP, int* pH) {
+ int seg = idx;
+ int f14 = (int)g->f14[seg];
+ int ang = s16(g->segAngle[seg]);
+ int thetaP = s16(ang - (int)((((std::int32_t)(f14 >> 1) * kInvPiQ16) << 1) >> 16));
+ std::int32_t relx = Wx - g->cx8[seg];
+ std::int32_t rely = Wy - g->cy8[seg];
+ std::int32_t c32, s32; q30_cossin(thetaP, &c32, &s32);
+ std::int64_t lat64 = (std::int64_t)c32 * relx - (std::int64_t)s32 * rely;
+ std::int64_t lon64 = (std::int64_t)s32 * relx + (std::int64_t)c32 * rely;
+ std::int32_t lat = (std::int32_t)(lat64 >> 30);
+ std::int32_t lon = (std::int32_t)(lon64 >> 30);
+ if (arg2 == 0) {
+ int d = s16(*pH - thetaP);
+ std::int32_t sn = cos30(s16(0x4000 - d));
+ std::int32_t cs = cos30(d);
+ std::int32_t tanterm = 0;
+ if (cs != 0) tanterm = (std::int32_t)(((std::int64_t)sn * lon) / cs);
+ *pP = s16(lat - tanterm);
+ return;
+ }
+ std::int64_t a2 = arg2;
+ std::int64_t disc = a2 * a2 - (std::int64_t)lon * lon;
+ std::int64_t T = (std::int64_t)sqrt64((std::uint64_t)disc);
+ std::int64_t Tsigned = (arg2 < 0) ? -T : T;
+ *pP = s16((std::int32_t)(lat - (std::int32_t)Tsigned));
+ std::int32_t Ts = (std::int32_t)Tsigned, lonv = lon;
+ std::uint32_t aa = (std::uint32_t)(arg2 < 0 ? -arg2 : arg2);
+ while (((aa >> 16) > 0) || ((aa >> 16) == 0 && (aa & 0xFFFF) >= 0x7F00)) {
+ Ts >>= 1; lonv >>= 1; aa >>= 1;
+ }
+ int at = atan2u((int)(std::int16_t)(Ts & 0xFFFF), (int)(std::int16_t)(lonv & 0xFFFF));
+ at = (arg2 < 0) ? s16(at + 0x4000) : s16(at - 0x4000);
+ *pH = s16(at + thetaP);
+}
+
+// Per-segment nudge of the world point W along a curvature ramp (commands whose
+// radius changes from segment to segment, i.e. a non-zero slope).
+void nudge(std::int32_t* pWx, std::int32_t* pWy, int H, std::int32_t arg2, std::int32_t slope) {
+ std::int32_t step = slope;
+ int a;
+ if (arg2 < 0) { step = -step; a = s16(H - 0x4000); }
+ else a = s16(H + 0x4000);
+ int sinA = gv(s16(0x4000 - a));
+ int cosA = gv(s16(a));
+ *pWx += (std::int32_t)(((std::int64_t)sinA * step) >> 14);
+ *pWy += (std::int32_t)(((std::int64_t)cosA * step) >> 14);
+}
+
+// Parse one command from the cc-line command stream.
+struct cc_cmd {
+ int end, word, N, a1, a2;
+ std::int32_t arg2, slope;
+ int arg1mul4, arg1if2is0;
+};
+void parse_cmd(const std::uint8_t* cc, int* po, cc_cmd* c) {
+ int o = *po;
+ std::memset(c, 0, sizeof *c);
+ int word = rdw(cc, o); o += 2;
+ c->word = word;
+ c->N = word & 0x7FF;
+ if (word == 0) { c->end = 1; *po = o; return; }
+ if (word & 0x8000) { c->a1 = rdw(cc, o); o += 2; c->a2 = rdw(cc, o); o += 2; }
+ int cx = rdw(cc, o); o += 2;
+ std::int32_t arg2 = (std::int32_t)(std::int16_t)rdw(cc, o); o += 2;
+ if (word & 0x4000) { arg2 = (std::int32_t)((std::uint32_t)arg2 << 16) | rdw(cc, o); o += 2; }
+ if (!(word & 0x1000)) arg2 <<= 3;
+ c->arg2 = arg2;
+ if (word & 0x2000) {
+ std::int32_t arg3 = (std::int32_t)(std::int16_t)rdw(cc, o); o += 2;
+ if (word & 0x4000) { arg3 = (std::int32_t)((std::uint32_t)arg3 << 16) | rdw(cc, o); o += 2; }
+ if (!(word & 0x1000)) arg3 <<= 3;
+ c->slope = (arg3 - arg2) / c->N;
+ }
+ if (arg2 != 0) { c->arg1if2is0 = 0; c->arg1mul4 = (std::int16_t)(cx << 2); }
+ else { c->arg1mul4 = 0; c->arg1if2is0 = cx; }
+ *po = o;
+}
+
+} // anonymous namespace
+
+// Walk the cc-line command stream: seed the lateral offset P and heading H, then
+// for each segment store its racing-line offset (bestLine) and heading delta
+// (angle18) before reprojecting the carried world point onto the next segment,
+// nudging it along on curvature ramps.
+int computeCcLine(const gp2cc_ccgeo& gref, const std::uint8_t* cc, int cc_len, int cc_off,
+ std::int16_t* bestLine, std::int16_t* angle18,
+ int* cmdStartSeg, int* pNumCmds) {
+ const gp2cc_ccgeo* g = &gref;
+ int n = g->n;
+ if (n <= 0) return -1;
+ int cur = 0, cmdIdx = 0;
+ int H = s16(g->segAngle[0]);
+ int P = 0;
+ int w0 = rdw(cc, cc_off);
+ // the initial lateral offset P is a SIGNED 16-bit word; sign-extend it.
+ if (!(w0 & 0x800) && (w0 & 0x8000)) P = s16(rdw(cc, cc_off + 2));
+ int o = cc_off;
+ for (;;) {
+ cc_cmd c;
+ parse_cmd(cc, &o, &c);
+ if (c.end) break;
+ if (o > cc_len) return -2;
+ if ((c.word & 0x800) && (c.word & 0x8000)) { P = s16(c.a1); H = s16(c.a2); }
+ if (c.arg2 == 0) H = s16(H + s16(c.arg1if2is0));
+ if (cmdStartSeg) cmdStartSeg[cmdIdx] = cur;
+ cmdIdx++;
+ std::int32_t Wx, Wy;
+ build_centre(g, cur, P, H, c.arg2, c.arg1mul4, &Wx, &Wy);
+ std::int32_t arg2 = c.arg2;
+ int left = c.N;
+ while (left > 0) {
+ angle18[cur] = (std::int16_t)s16(H - s16(g->segAngle[cur]));
+ bestLine[cur] = (std::int16_t)s16(P);
+ cur = (cur + 1) % n;
+ reproject(g, cur, Wx, Wy, arg2, &P, &H);
+ left--;
+ if (left == 0) break;
+ if (arg2 != 0 && c.slope != 0) {
+ arg2 += c.slope;
+ nudge(&Wx, &Wy, H, arg2, c.slope);
+ }
+ }
+ }
+ if (pNumCmds) *pNumCmds = cmdIdx;
+ return 0;
+}
+
+} // namespace gp2geom
diff --git a/gp2ccline.hpp b/gp2ccline.hpp
new file mode 100644
index 0000000..30fe4ad
--- /dev/null
+++ b/gp2ccline.hpp
@@ -0,0 +1,24 @@
+#pragma once
+#include
+#include "gp2cc.h" // gp2cc_ccgeo input struct
+
+// ---------------------------------------------------------------------------
+// gp2ccline — GP2's racing-line (cc-line) solver, ported to C++ and fed by the
+// formula-generated cosine/atan tables (gp2cos.hpp / gp2atan.hpp).
+//
+// This is the CHAOTIC kernel: a reproject/nudge feedback loop whose output
+// diverges on any rounding difference. It therefore reproduces the engine's
+// fixed-point math exactly, rather than re-deriving it — kept legible with
+// comments and meaningful names, but every integer operation is load-bearing
+// and must not be "simplified". Validated bit-exact against the GP2Lap dumps.
+// ---------------------------------------------------------------------------
+namespace gp2geom {
+
+// Run the cc-line pass. Writes the per-segment racing-line offset (bestLine) and
+// heading delta (angle18); cmdStartSeg/pNumCmds (may be null) record each
+// cc-command's first segment. Returns 0 on success.
+int computeCcLine(const gp2cc_ccgeo& g, const std::uint8_t* cc, int cc_len, int cc_off,
+ std::int16_t* bestLine, std::int16_t* angle18,
+ int* cmdStartSeg, int* pNumCmds);
+
+} // namespace gp2geom
diff --git a/gp2cos.hpp b/gp2cos.hpp
new file mode 100644
index 0000000..cbd48a9
--- /dev/null
+++ b/gp2cos.hpp
@@ -0,0 +1,94 @@
+#pragma once
+#include
+#include
+
+// ---------------------------------------------------------------------------
+// GP2 geometry — the cosine table expressed as its generating formula.
+//
+// GP2 represents angles in "binary radians": 65536 units per full turn, so a
+// quarter turn is 0x4000. The engine keeps a cosine table of amplitude 0x4000
+// (16384), sampled every 8 units, and for the position step it does a RAW,
+// non-interpolated lookup COS[angle >> 3]. That lookup is therefore the cosine
+// of the angle quantised to the 8-unit grid:
+//
+// COS[i] = round( 16384 * cos( i * pi / 4096 ) ) (i = angle>>3)
+//
+// ...with exactly ONE exception. At i = 1992 the exact value is 703.5004 —
+// dead on a .5 rounding boundary — and GP2's own table generator resolved it
+// to 703 (round() gives 704). That single 1-unit entry is load-bearing: the
+// racing-line solver is a chaotic feedback system, and a track whose heading
+// passes through this angle (e.g. F1CT09) drops from 100% to 29% correct
+// without it. So we override that one entry explicitly (kQuirkIndex below).
+//
+// Why a formula instead of an embedded blob: the formula documents intent
+// (this IS a cosine) and computing it at build time keeps the runtime fully
+// deterministic (no per-call float).
+//
+// WARNING: the entries come from compile-time floating-point rounding, so a
+// different compiler or standard library could round an entry that lands on a
+// .5 boundary the other way. We force only the one known boundary case
+// (i = 1992); any other toolchain disagreement would be silent. If the compiled
+// track or cc-line ever start to DRIFT, suspect a rounding mismatch in this
+// table (or in gp2atan.hpp) first: regenerate it and diff against GP2's t_Sinus
+// bytes.
+// ---------------------------------------------------------------------------
+
+namespace gp2geom {
+
+inline constexpr int kGrid = 4098; // indices 0..4097 (cos over [0, pi])
+inline constexpr short kAmplitude = 16384; // 0x4000
+inline constexpr int kQuirkIndex = 1992; // the lone .5-boundary entry GP2 rounds down
+inline constexpr short kQuirkValue = 703;
+inline constexpr double kPi = 3.14159265358979323846;
+
+// constexpr cosine (std::cos isn't portably constexpr before C++26): fold the
+// argument into [0, pi/2] via cos(x) = -cos(pi - x), then a Taylor series.
+// Accurate to ~1e-15 over [0, pi] — far inside the rounding margin (the closest
+// non-quirk entry sits >1e-4 from a .5 boundary).
+constexpr double cosine(double x) {
+ bool negate = false;
+ if (x > kPi * 0.5) { x = kPi - x; negate = true; }
+ double term = 1.0, sum = 1.0, x2 = x * x;
+ for (int n = 1; n < 16; ++n) {
+ term *= -x2 / static_cast((2 * n - 1) * (2 * n));
+ sum += term;
+ }
+ return negate ? -sum : sum;
+}
+
+// round half away from zero, matching the game's table.
+constexpr short roundToShort(double v) {
+ long r = (v >= 0.0) ? static_cast(v + 0.5) : static_cast(v - 0.5);
+ return static_cast(r);
+}
+
+// Build the table at compile time from the formula + the one documented quirk.
+constexpr std::array makeCosineTable() {
+ std::array t{};
+ for (int i = 0; i < kGrid; ++i) {
+ t[i] = (i == kQuirkIndex)
+ ? kQuirkValue
+ : roundToShort(kAmplitude * cosine(i * (kPi / 4096.0)));
+ }
+ return t;
+}
+
+inline constexpr std::array kCosine = makeCosineTable();
+
+// Raw (non-interpolated) cosine of an angle in binary-radian units: cos is
+// even, so fold the sign, then index the 8-unit grid. This is the readable
+// equivalent of GP2's COS[((a>>2)&0xFFFE)>>1].
+constexpr short scos(int angle) {
+ int a = static_cast(angle & 0xFFFF); // wrap to signed 16-bit
+ if (a < 0) a = -a;
+ return kCosine[a >> 3];
+}
+
+// Cheap spot-checks (no embedded reference table). The 1992 entry is the one
+// load-bearing rounding boundary, so we still pin it at compile time.
+static_assert(kCosine[0] == 16384, "cos(0)");
+static_assert(kCosine[2048] == 0, "cos(pi/2)");
+static_assert(kCosine[4096] == -16384, "cos(pi)");
+static_assert(kCosine[1992] == 703, "GP2 .5-boundary quirk");
+
+} // namespace gp2geom
diff --git a/gp2geom.cpp b/gp2geom.cpp
new file mode 100644
index 0000000..6772356
--- /dev/null
+++ b/gp2geom.cpp
@@ -0,0 +1,261 @@
+#include "gp2geom.hpp"
+
+// Cosine source: the build-time-verified formula table (gp2cos.hpp), generated
+// at compile time and static_assert'd against GP2's bytes.
+#include "gp2cos.hpp"
+
+namespace gp2geom {
+namespace {
+
+// ============================== units & model ===============================
+//
+// ANGLES are binary radians: 65536 units per full turn, so a quarter turn is
+// 0x4000. Wrapping is automatic in 16 bits and trig is a direct table lookup
+// with no range reduction. The heading is integrated in Q16 fixed point — the
+// whole-unit angle is the top 16 bits of a 32-bit accumulator.
+constexpr int kQuarterTurn = 0x4000; // 90 degrees
+constexpr int kQ16 = 16; // heading fractional bits
+
+// Each segment is a FIXED-LENGTH straight chord. The engine steps 128 world
+// units per segment; in its 1/8-world-unit grid that is 1024.
+constexpr int kChordEighths = 0x400; // 1024 = 128 world units, in 1/8 units
+constexpr int kChordShift = 14; // chord = (cos * kChordEighths) >> 14
+
+// The cc-line's per-segment "theta-prime" tilt (tseg+0x14) is the section's
+// per-segment turn scaled by round(pi/2 * 2^14). Produced by the geometry pass,
+// consumed later by the racing-line solver.
+constexpr int kHalfPiQ14 = 0x6488; // round( (pi/2) * 2^14 ) = 25736
+
+// side vector = unit perpendicular to the start heading, scaled by road width;
+// one component = ((trig * width) >> 17) << 6 (InitTrackSegs 0x796C3/0x79735).
+
+// signed 16-bit wrap (binary-radian angles live in 16 bits).
+constexpr int wrap16(int v) { v &= 0xFFFF; return (v & 0x8000) ? v - 0x10000 : v; }
+
+// raw, grid-sampled cosine (amplitude 0x4000) of a binary-radian angle.
+inline int rawCos(int a) {
+ return scos(a); // formula table (gp2cos.hpp)
+}
+inline int cosine(int angle) { return rawCos(angle); }
+inline int sine (int angle) { return rawCos(kQuarterTurn - angle); } // sin(x) = cos(90-x)
+
+// little-endian readers.
+inline int rdU16(const std::uint8_t* d, int o) { return d[o] | (d[o + 1] << 8); }
+inline int rdS16(const std::uint8_t* d, int o) { return wrap16(rdU16(d, o)); }
+inline std::int32_t rdS32(const std::uint8_t* d, int o) {
+ return (std::int32_t)(d[o] | (d[o+1] << 8) | (d[o+2] << 16) | (d[o+3] << 24));
+}
+
+// Track-command argument sizes (datparse). Opcode 0x45 is conditional on a c5
+// bit and is handled separately; -1 means an unknown opcode (stream desync).
+constexpr int kArgSize[0x66] = {
+ 2,2,2,0,0,4,0,0,2,2, 10,10,2,2,4,4,2,2,2,2, 2,2,0,0,2,2,4,0,0,0,
+ 0,0,0,0,0,0,0,0,4,4, 0,2,6,4,8,4,2,4,2,2, 2,2,4,4,2,2,26,2,4,8,
+ 14,4,4,4,2,2,4,4,6,14, 2,4,14,16,8,8,4,6,2,2, 4,0,2,2,0,2,0,0,0,2,
+ 2,0,2,4,6,6,2,0,0,0, 0,0 /* 0x64, 0x65 */
+};
+
+enum Side { Left, Right };
+
+// ============================== the turtle ==================================
+class TrackBuilder {
+public:
+ TrackBuilder(const std::uint8_t* dat, gp2cc_track& out) : d(dat), t(out) {}
+
+ int run() {
+ readHeader();
+ if (!walkSections()) return -2; // turn + half-step + width, per segment
+ t.n = segCount - 1; // drop the over-produced wrap segment
+ computeSideVectors(); // road-edge perpendiculars
+ integratePositions(); // advance one fixed chord per segment
+ distributeClosureGap(); // DDA warp so the lap closes exactly
+ return 0;
+ }
+
+private:
+ const std::uint8_t* d;
+ gp2cc_track& t;
+
+ // turtle state during the section walk
+ std::int32_t heading = 0; // d_TrkStrtAngle, Q16 binary radians
+ std::int32_t sectionStartHeading = 0; // d_StartAngle, the side-vector reference
+ int widthL = 0, widthR = 0; // current road width (left/right halves)
+ int dWidthL = 0, dWidthR = 0; // per-segment width ramp deltas
+ int rampL = 0, rampR = 0; // segments left in each width transition
+ bool halfStepFull = false; // c5 bit12: full-precision half-step
+ bool c5bit9 = false; // c5 bit9: opcode-0x45 arg size
+ int segCount = 0;
+ int headerEnd = 0;
+
+ // -- header: start pose, widths, flags; cursor lands at the command stream --
+ void readHeader() {
+ const int base = 0x1020 + rdS32(d, 0x1010); // MainTrackData
+ t.hdr_angle = (std::int16_t)rdS16(d, base + 0);
+ t.hdr_Y = (std::int16_t)rdS16(d, base + 4);
+ t.hdr_X = (std::int16_t)rdS16(d, base + 8);
+ t.hdr_width = (std::uint16_t)rdU16(d, base + 0xA);
+ t.hdr_c5 = (std::uint16_t)rdU16(d, base + 0xE);
+ halfStepFull = (t.hdr_c5 & 0x1000) != 0;
+ c5bit9 = (t.hdr_c5 & 0x0200) != 0;
+ const int unknownCount = rdU16(d, base + 0x12);
+
+ heading = (std::int32_t)((t.hdr_angle & 0xFFFF) << kQ16);
+ sectionStartHeading = heading;
+ widthL = widthR = (int)t.hdr_width;
+
+ int o = base + 0x14; // skip the "4 unknowns" block
+ for (int i = 0; i < unknownCount; ++i) { int ax = rdU16(d, o); o += 2; if (ax != 0x1F) o += 2; }
+ headerEnd = o;
+ }
+
+ // -- walk the command stream: op commands tweak width; geometry commands are
+ // sections the turtle drives through. 0xFFFF ends it (cc-line stream next). --
+ bool walkSections() {
+ int o = headerEnd;
+ for (;;) {
+ const int w = rdU16(d, o);
+ if (w == 0xFFFF) { t.ccoff = o + 2; return true; }
+ if (w & 0x8000) { o = applyOpCommand(o, w); if (o < 0) return false; }
+ else { o = buildSection(o, w); if (o < 0) return false; }
+ }
+ }
+
+ int applyOpCommand(int o, int w) {
+ const int op = (w >> 8) & 0x7F;
+ const int sz = (op == 0x45) ? (c5bit9 ? 14 : 12)
+ : (op < 0x66) ? kArgSize[op]
+ : -1;
+ if (sz < 0) return -1; // unknown opcode -> desync
+ if (op == 0x34 || op == 0x05) setWidthTransition(Left, rdS16(d, o + 2), rdS16(d, o + 4));
+ if (op == 0x35 || op == 0x05) setWidthTransition(Right, rdS16(d, o + 2), rdS16(d, o + 4));
+ return o + 2 + sz;
+ }
+
+ // -- one geometry command = one section of `segments` fixed chords, all
+ // turning by the same per-segment amount. --
+ int buildSection(int o, int segments) {
+ const int perSegTurn = rdS16(d, o + 2); // d1word
+ const std::int32_t turn = (std::int32_t)perSegTurn << kQ16;
+ // theta'-tilt the cc-line will need later (computed from this command).
+ const std::int32_t f14 = ((std::int32_t)perSegTurn * kHalfPiQ14) >> 14;
+ o += 10;
+
+ // MIDPOINT INTEGRATION RULE: the heading is sampled at each segment's
+ // midpoint, so a section starts and ends with a HALF turn and takes full
+ // turns in between. This centres each straight chord on the underlying arc
+ // (no corner-cutting) and makes curvature continuous across section joins:
+ // a section's end-half plus the next section's start-half = one full turn.
+ std::int32_t halfTurn = turn >> 1;
+ // bit12-clear tracks zero only the LOW 16 bits of the half-step (the engine's
+ // `xor ax,ax`), i.e. they floor d1/2 to a whole binary-radian unit.
+ if (!halfStepFull) halfTurn &= ~0xFFFF;
+
+ for (int k = 0; k < segments; ++k) {
+ if (segCount >= GP2CC_MAXSEG) return -1;
+ if (k == 0) { sectionStartHeading = heading; heading += halfTurn; } // start half-step
+ else { heading += turn; sectionStartHeading += turn; } // full turn
+ emitSegment(f14);
+ }
+ heading += halfTurn; // end half-step (pairs with the next start half)
+ return o;
+ }
+
+ void emitSegment(std::int32_t f14) {
+ const int i = segCount++;
+ t.fAngle[i] = heading;
+ t.angle[i] = (std::int16_t)(heading >> kQ16);
+ t.startAngle[i] = (std::int16_t)(sectionStartHeading >> kQ16);
+ t.f14[i] = f14;
+ t.widthL[i] = widthL; // store current width, THEN advance the ramp
+ t.widthR[i] = widthR;
+ rampWidthOneStep();
+ }
+
+ // -- width transitions: len==0 sets it instantly, len>0 ramps linearly. --
+ void setWidthTransition(Side s, int len, int target) {
+ int& cur = (s == Left) ? widthL : widthR;
+ int& delta = (s == Left) ? dWidthL : dWidthR;
+ int& ramp = (s == Left) ? rampL : rampR;
+ if (len == 0) { cur = target; delta = 0; ramp = 0; }
+ else { ramp = len; delta = (target - cur) / len; } // truncating step
+ }
+ void rampWidthOneStep() {
+ if (rampL > 0) { widthL += dWidthL; --rampL; }
+ if (rampR > 0) { widthR += dWidthR; --rampR; }
+ }
+
+ // -- road edges = centre +/- sideVector; sideVector is perpendicular to the
+ // start heading, scaled by width. --
+ void computeSideVectors() {
+ const int n = t.n;
+ for (int i = 0; i < n; ++i) {
+ const int sa = wrap16(t.startAngle[i]);
+ const int cs = cosine(sa), sn = sine(sa);
+ t.sideRX[i] = component(cs, t.widthR[i]);
+ t.sideRY[i] = component(sn, t.widthR[i]);
+ t.sideLX[i] = component(cs, t.widthL[i]);
+ t.sideLY[i] = component(sn, t.widthL[i]);
+ }
+ }
+ static std::int16_t component(int trig, int width) {
+ // ((trig*width) >> 16) keeps the perpendicular unit*width, >>1 halves it
+ // and <<6 puts it in the engine's edge units. Folded through int16 like
+ // the engine's word ops.
+ return (std::int16_t)(((std::int16_t)((int)trig * width >> 16) >> 1) << 6);
+ }
+
+ // -- advance one fixed 128-unit chord per segment along the heading (forward
+ // Euler), accumulating the raw, not-yet-closed position. --
+ void integratePositions() {
+ std::int32_t x = (std::int32_t)t.hdr_X << 3; // start position, 1/8 world units
+ std::int32_t y = (std::int32_t)t.hdr_Y << 3;
+ const int n = t.n;
+ for (int i = 0; i < n; ++i) {
+ const int h = t.fAngle[i] >> kQ16;
+ t.rawX8[i] = x; t.rawY8[i] = y;
+ x += (cosine(h) * kChordEighths) >> kChordShift; // X step = cos(h) * 128
+ y += (sine(h) * kChordEighths) >> kChordShift; // Y step = sin(h) * 128
+ }
+ t.gapX8 = x - t.rawX8[0]; // raw integration doesn't close the loop exactly
+ t.gapY8 = y - t.rawY8[0];
+ }
+
+ // -- close the loop: the chord rounding leaves a gap between end and start.
+ // Spread it backward with a DDA sweep so segment i carries
+ // sign(gap)*floor(i*|gap|/M), M = n+1 (the engine counts the wrap segment
+ // we dropped). First segment ~0, last ~the whole gap -> the lap closes. --
+ void distributeClosureGap() {
+ const int n = t.n;
+ const std::int64_t M = (std::int64_t)n + 1;
+ const std::int64_t absX = t.gapX8 < 0 ? -(std::int64_t)t.gapX8 : t.gapX8;
+ const std::int64_t absY = t.gapY8 < 0 ? -(std::int64_t)t.gapY8 : t.gapY8;
+ for (int i = 0; i < n; ++i) {
+ const std::int32_t sx = (std::int32_t)((absX * i) / M);
+ const std::int32_t sy = (std::int32_t)((absY * i) / M);
+ t.X8[i] = t.rawX8[i] - (t.gapX8 < 0 ? -sx : sx);
+ t.Y8[i] = t.rawY8[i] - (t.gapY8 < 0 ? -sy : sy);
+ }
+ }
+};
+
+} // anonymous namespace
+
+int compileGeometry(const std::uint8_t* dat, int len, gp2cc_track& out) {
+ (void)len;
+ TrackBuilder builder(dat, out);
+ return builder.run();
+}
+
+// GP2's GetSinusVal (0x104B9): fold the sign, sample the 8-unit cosine grid, and
+// linearly interpolate over the low 3 bits. Bit-identical to the old C gp2cc_gv.
+int gv(int angle) {
+ int ax = wrap16(angle);
+ if (ax < 0) ax = (-ax) & 0xFFFF;
+ int frac = ax & 7;
+ int widx = ((ax >> 2) & 0xFFFE) >> 1; // == ax >> 3
+ int base = kCosine[widx];
+ int d = wrap16(kCosine[widx + 1] - base);
+ return wrap16(base + ((d * frac) >> 3));
+}
+
+} // namespace gp2geom
diff --git a/gp2geom.hpp b/gp2geom.hpp
new file mode 100644
index 0000000..3707341
--- /dev/null
+++ b/gp2geom.hpp
@@ -0,0 +1,28 @@
+#pragma once
+#include
+#include "gp2cc.h" // shared output format: gp2cc_track + GP2CC_MAXSEG
+
+// ---------------------------------------------------------------------------
+// gp2geom — GP2 track-geometry compiler, written as the geometric process it
+// actually is (a fixed-point "turtle" that integrates a curve from its
+// curvature), rather than as a transcription of the disassembly.
+//
+// Same bit-exact integers as the engine, but the steps are named — turn,
+// advance one chord, midpoint half-step, ramp width, close the loop — and every
+// "magic" constant is derived in a comment.
+//
+// The cosine it uses is the build-time-verified table from gp2cos.hpp.
+// ---------------------------------------------------------------------------
+namespace gp2geom {
+
+// Compile a track .dat image already in memory into `out`. Returns 0 on success,
+// negative on a command-stream desync. Bit-exact with the game.
+int compileGeometry(const std::uint8_t* dat, int len, gp2cc_track& out);
+
+// Interpolated cosine (amplitude 0x4000) of a binary-radian angle, matching
+// GP2's GetSinusVal (0x104B9): an 8-unit grid lookup with a linear lerp over the
+// low 3 bits. The editor uses this only to draw perpendiculars/ticks for the
+// overlay; sin(x) is gv(0x4000 - x).
+int gv(int angle);
+
+} // namespace gp2geom
diff --git a/res/bmp00076.bmp b/res/bmp00076.bmp
index b4a794b..958f5ff 100755
Binary files a/res/bmp00076.bmp and b/res/bmp00076.bmp differ
diff --git a/resource.h b/resource.h
old mode 100755
new mode 100644
index 295ad6b..5ebe9c4
--- a/resource.h
+++ b/resource.h
@@ -885,6 +885,8 @@
#define ID_HELP_QUICKKEYREMINDER 33097
#define ID_SHOW_SHOWSCENERYARMS 33098
#define ID_SHOW_USESWIVELANGLES 33099
+#define ID_VIEW_COMPILED 33100
+#define ID_VIEW_COMPILED_CCLINE 33101
#define ID_INDICATOR_MODEL 59142
// Next default values for new objects
@@ -893,7 +895,7 @@
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_3D_CONTROLS 1
#define _APS_NEXT_RESOURCE_VALUE 266
-#define _APS_NEXT_COMMAND_VALUE 33100
+#define _APS_NEXT_COMMAND_VALUE 33102
#define _APS_NEXT_CONTROL_VALUE 1244
#define _APS_NEXT_SYMED_VALUE 115
#endif