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