/* * Copyright (c) 2024 Scott Duensing, scott@kangaroopunch.com * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /* * Memory Map: * * Far Heap (upper 448k): * * 0x7ffff - Top of far heap / End of bitmap page 1 * - Bitmap page is 0x14000 (81920 bytes) * 0x6c000 - Start of bitmap page 1 * 0x6bfff - End of bitmap page 2 * - Bitmap page is 0x14000 (81920 bytes) * 0x58000 - Start of bitmap page 2 * 0x57fff - End of A2-3D2 and data to be used in slots 3 and 2/4 below * - ... * 0x54000 - Start of A2-3D2 and data to be used in slots 3 and 2/4 below * - 57344 bytes free * 0x45fff - End of overlay code * - ... * 0x10000 - Bottom of far heap / Start of overlay code * * * Near Heap (lower 64k): * * 0xffff - Top of near heap * - ... * 0xe000 - Microkernel * - ... * 0xc000 - I/O and microkernel * - ... * 0xa000 - Overlay code * 0x9fff - Top of virtual stack * - ... * 0x8000 - Program code / 3D/2D data * - ... * 0x6000 - Program code / 3D library * - ... * 0x0300 - Start of program code * 0x02ff - End of program arguments / data storage * - ... (We're going to overwrite this!) * 0x0200 - Start of program arguments / data storage * 0x01ff - Top of CPU stack space * - ... * 0x0100 - Bottom of CPU stack space * 0x00ff - End of microkernel use * - ... * 0x00f0 - Start of microkernel use * - 46 bytes free * 0x00c2 - End of A2-3D2 use * - ... * 0x0060 - Start of A2-3D2 use * - 49 bytes free * 0x002f - llvm virtual register 31 * - ... * 0x0010 - llvm virtual register 0 * 0x000f - MMU_MEM_BANK_7 * - ... * 0x0008 - MMU_MEM_BANK_0 * - 7 bytes free * 0x0001 - MMU_IO_CTRL * 0x0000 - MMU_MEM_CTRL * * * Calling the 3D Renderer: * * The MMU splits the near heap into 8k "slots" that 8k "blocks" of far heap * can be mapped into. f256lib, by default, sets the near heap up as * follows: * * 0xe000 - Slot 7 - Microkernel * 0xc000 - Slot 6 - I/O and Microkernel * 0xa000 - Slot 5 - Code Overlay Region * 0x8000 - Slot 4 - Program Code * 0x6000 - Slot 3 - Program Code * 0x4000 - Slot 2 - Program Code * 0x2000 - Slot 1 - Program Code * 0x0000 - Slot 0 - Zero Page & Program Code * * Our 3D library, subLOGIC's A2-3D2, was designed to be run on an Apple ][. * The Apple ][ has two high-resolution video pages in the middle of the * memory map and A2-3D2 loads at 0x6000 immediately after the second video * page. This makes sense on the Apple. On the Foenix, it's in the way at * slot 3. The library also needs a 3D scene database and outputs an array * of 2D drawing instructions. Given the size of A2-3D2 this pushes our * memory usage up into slot 4. This leaves slots 0 to 2 for the simulator. * That's less than 24k. Not good. * * We're going to place the 3D library and related data in the far heap and * manually swap them in using the MMU when we need to render the scene. To * do this, we need to ensure we're not currently executing code from * somewhere in the slot 2 to 4 range. To accomplish this, all the functions * that deal with A2-3D2 are placed in an overlay. This ensures whatever * code calls into slots 2 to 4 has a program counter between 0xa000 and * 0xbfff - well out of the way of the 3D stuff. * * When enabling compiler optimizations, both the compiler and A2-3D2 want * use of the remaining zero page. We maintain copies of the area that * gets clobbered by each and make sure the right one is in place at the * right time. * * The last thing to worry about is data needed by both the simulator and * A2-3D2. This is basically only the camera position and rotation. These * 10 bytes will be stored directly into RAM from 0x200 to 0x20a overwriting * any command line arguments that may have been in memory. * */ #include #ifdef __F256__ #define WITHOUT_FILE #define WITHOUT_SPRITE #define WITHOUT_TILE #define F256LIB_IMPLEMENTATION #include "f256lib.h" /* * embedded.bin is loaded at 0x54000: * * 8k a2-3d2.bin @ 0x54000 * 8k scene.3d @ 0x56000 * */ EMBED(embedded, "embedded.bin", 0x54000); #endif #include "a23d2.h" asm ( ".text\n" // " .cpu mosw65c02\n" // "mos-insns-w65c02\n" // "EF_MOS_ARCH_W65C02 \n" ".global dmaClear\n" "dmaClear:\n" "pha\n" // "phx\n" ".byte $DA\n" "php \n" // preserver interrupt state + c "sei \n" // disable interrupts "ldy 1 \n" // y = io_ctrl //"phy \n" // save io_ctrl on stack ".byte $5A\n" //"stz 1 \n" // map in the dma registers, where we can get them ".byte $64,$01 \n" //"stz $DF00 \n" // clear dma control ".byte $9c,$00,$DF\n" "ldy #$05 \n" // DMA_CTRL_ENABLE+DMA_CTRL_FILL "sty $DF00 \n" "ldy __rc8 \n" // Fill Color "sty $DF01 \n" // Fill value "sta $DF08 \n" // Addr Low "stx $DF09 \n" // Addr Hi "lda __rc2 \n" // Addr Bank "sta $DF0A \n" "lda __rc4 \n" // length low "sta $DF0C \n" "lda __rc5 \n" // length middle "sta $DF0D \n" "lda __rc6 \n" // length high "sta $DF0E \n" "lda #$80 \n" // DMA_CTRL_START //"tsb $DF00 \n" // DMA_CTRL ".byte $0C,$00,$DF\n" //"wait_start: lda $DF01 \n" // DMA_STATUS // "bpl wait_start \n" // Wait for DMA "wait_end: lda $DF01 \n" // DMA_STATUS "bmi wait_end \n" // Wait for DMA //"stz $DF00 \n" // DMA Off ".byte $9c,$00,$DF\n" "nop \n" "nop \n" "nop \n" // wait "nop \n" "nop \n" "nop \n" "pla \n" // restore the io_ctrl "sta 1 \n" "plp \n" // "plx\n" ".byte $FA\n" "pla\n" "rts \n" ); void dmaClear(uint32_t address, uint32_t length, byte color); #if 0 typedef struct airplaneS { int8_t aileron; // -15 to 15 int8_t elevator; // -15 to 15 int8_t rudder; // -15 to 15 int8_t throttle; // -15 to 15 bool ignition; bool engine; int16_t rpm; float hSpeed; float vSpeed; float deltaZ; float efAOF; float climbRate; bool airborne; bool stall; bool brake; int16_t x; int16_t y; int16_t z; double pitch; // 0 to 255 double yaw; // 0 to 255 double roll; // 0 to 255 } airplaneT; airplaneT _plane; byte _gamepad; #endif #if 0 //#define SEGMENT_MATH #define PI 3.1415926 //#define Rads(d) (((d) < 0 ? (d) + 360 : (d)) * (PI / 180)) #define Degs(r) ((r) * (180 / PI)) // https://www.reddit.com/r/programming/comments/3e7ghi/discrete_arctan_in_6502/ #define ATAN_SPLINE_C0 (double)(-0.14380550980765507115) /* 3pi/4 - 5/2 */ #define ATAN_SPLINE_C1 (double)(-0.07079632679489661923) /* 3/2 - pi/2 */ double atan(double x){ if (x >= 0) { if ( x<= 1) { return x + (ATAN_SPLINE_C0 + ATAN_SPLINE_C1 * x) * x * x; } else { x = 1 / x; return PI / 2 - (x + (ATAN_SPLINE_C0 + ATAN_SPLINE_C1 * x) * x * x); } } else { if (x >= -1) { return x - (ATAN_SPLINE_C0 - ATAN_SPLINE_C1 * x) * x * x; } else { x = -1 / x; return (x + (ATAN_SPLINE_C0 + ATAN_SPLINE_C1 * x) * x * x) - PI / 2; } } } float sin(float d) { _tdata = d; a23d2Sin(); return _trig; } float cos(float d) { _tdata = d; a23d2Cos(); return _trig; } //#define SEGMENT_INPUT void getInput(void) { static byte oneJoy = 0; static byte twoJoy = 0; static byte keyJoy = 0; do { kernelCall(NextEvent); // Read real joysticks. if (kernelEventData.type == kernelEvent(GAME)) { oneJoy = kernelEventData.game.game0; twoJoy = kernelEventData.game.game1; } //***TODO*** Add SNES/NES once I get some. // Use keyboard as virtual joystick. if (kernelEventData.type == kernelEvent(key.PRESSED)) { switch (kernelEventData.key.ascii) { case 'w': case 'W': keyJoy |= JOY_UP; break; case 'a': case 'A': keyJoy |= JOY_LEFT; break; case 's': case 'S': keyJoy |= JOY_DOWN; break; case 'd': case 'D': keyJoy |= JOY_RIGHT; break; case 'j': case 'J': keyJoy |= JOY_BUTTON_1; break; case 'k': case 'K': keyJoy |= JOY_BUTTON_2; break; case 'l': case 'L': keyJoy |= JOY_BUTTON_3; break; } } if (kernelEventData.type == kernelEvent(key.RELEASED)) { switch (kernelEventData.key.ascii) { case 'w': case 'W': keyJoy &= ~JOY_UP; break; case 'a': case 'A': keyJoy &= ~JOY_LEFT; break; case 's': case 'S': keyJoy &= ~JOY_DOWN; break; case 'd': case 'D': keyJoy &= ~JOY_RIGHT; break; case 'j': case 'J': keyJoy &= ~JOY_BUTTON_1; break; case 'k': case 'K': keyJoy &= ~JOY_BUTTON_2; break; case 'l': case 'L': keyJoy &= ~JOY_BUTTON_3; break; } } } while (kernelGetPending() > 0); // Merge inputs. Yes, this allows dumb things like LEFT and RIGHT at the same time. _gamepad = oneJoy | twoJoy | keyJoy; } //#define SEGMENT_FLIGHT_MODEL_1 // These didn't use to be global. Ugly. float tmpX; float tmpY; float tmpZ; float newX; float newY; float newZ; float iSpeed; float lSpeed; float hAccel; float lVeloc; float gVeloc; float deltaZ; float AOA; float torque; float torque2; uint16_t loopTime = 40; // Used to be static. Need preserved. double dPitch = 0; double dYaw = 0; double dRoll = 0; float collectX = 0; float collectY = 0; float collectZ = 0; // This flight model is basically the one from "Build Your Own Flight Sim // in C++" by Michael Radtke and Chris Lampton. void lightPlane(void) { // Flight model. Power Dynamics. if (_plane.ignition) { // Start engine. if (!_plane.engine) _plane.engine = true; // Adjust RPM. if (_plane.rpm < (375 + (_plane.throttle * 117))) _plane.rpm += loopTime * 0.5; //***TODO*** T if (_plane.rpm > (375 + (_plane.throttle * 117))) _plane.rpm -= loopTime * 0.5; } else { // Stop engine. if (_plane.engine) _plane.engine = false; // Run down engine. if (_plane.rpm > 0) _plane.rpm -= (int16_t)(loopTime / 2); //***TODO*** This will never work. if (_plane.rpm < 0) _plane.rpm = 0; } // Flight model. Flight Dynamics. // Calculate speed from RPM. iSpeed = _plane.rpm / 17.5; // Modify speed by pitch. iSpeed += (_plane.pitch * 1.5); // Horizontal acceleration - thrust. hAccel = ((_plane.rpm * (iSpeed - _plane.hSpeed)) / 10000); hAccel /= 1000; hAccel *= loopTime; if (_plane.brake && !_plane.airborne) { // Handle brakes. if (_plane.hSpeed > 0) { _plane.hSpeed -= 1; } else { _plane.hSpeed = 0; } } else { // Accelerate normally. _plane.hSpeed += hAccel; } // Force speed to range -1..1. lSpeed = (_plane.hSpeed / 65) - 1; if (lSpeed > 1) lSpeed = 1; // Lift curve. lVeloc = Degs(atan(lSpeed)); // Force lift to range 0..90. lVeloc += 45; // Shift range to 0..-17. lVeloc /= 5.29; // Multiply by pitch modifier. lVeloc *= (-(_plane.pitch * .157) + 1); // Time slice. lVeloc /= 1000; lVeloc *= loopTime; // Gravity. gVeloc = loopTime * (-16.0 / 10000); // -16.0 is ft/sec for gravity. // Sum vertical velocity. _plane.vSpeed = gVeloc + lVeloc; // No vertical speed if we're on the ground. if (!_plane.airborne && (_plane.vSpeed < 0)) _plane.vSpeed = 0; // Save climb rate in ft/min. _plane.climbRate = _plane.vSpeed / loopTime; _plane.climbRate *= 60000L; // Expand to ft/hr. deltaZ = _plane.hSpeed * 5280; // Get ft/ms. deltaZ /= 3600000L; deltaZ *= loopTime; // Find effective angle of flight. if (deltaZ) { _plane.efAOF = -(atan(_plane.vSpeed / deltaZ)); } else { _plane.efAOF = -(atan(_plane.vSpeed)); } // Convert to degrees. AOA = Degs(_plane.efAOF); // Handle stalling. if (((_plane.pitch < AOA) && (AOA < 0)) && (_plane.hSpeed < 40)) { if ((_plane.pitch - AOA) < -20) _plane.stall = true; } if (_plane.stall) { if (_plane.pitch > 30) { _plane.stall = false; } else { _plane.pitch++; } } } //#define SEGMENT_FLIGHT_MODEL_2 void lightPlane2(void) { // Flight model. Inertial Damping. if (dPitch) { dPitch -= dPitch / 10; if (((dPitch > 0) && (dPitch < 0.01)) || ((dPitch < 0) && (dPitch > -0.01))) dPitch = 0; } if (dYaw) { dYaw -= dYaw / 10; if (((dYaw > 0) && (dYaw < 0.01)) || ((dYaw < 0) && (dYaw > -0.01))) dYaw = 0; } if (dRoll) { dRoll -= dRoll / 10; if (((dRoll > 0) && (dRoll < 0.01)) || ((dRoll < 0) && (dRoll > -0.01))) dRoll = 0; } // Flight model. Rate of Change. if (_plane.airborne) { if (_plane.aileron != 0) { torque = ((_plane.hSpeed * _plane.aileron) / 10000); if (dRoll != (torque * loopTime)) dRoll += torque * 6; } } if (_plane.elevator != 0) { torque = ((_plane.hSpeed * _plane.elevator) / 10000); if ((!_plane.airborne) && (torque > 0)) torque = 0; //***FIX*** This is dumb. if (dPitch != (torque * loopTime)) dPitch += torque * 1.5; } if (_plane.hSpeed) { torque = 0.0; if (_plane.rudder != 0) torque -= ((_plane.hSpeed * _plane.rudder) / 10000); torque2 = 0.0; if ((_plane.roll > 0) && (_plane.roll <= 90)) { //***FIX*** This is dumb. torque2 = _plane.roll * 0.00050; } else { if ((_plane.roll < 0) && (_plane.roll >= -90)) { torque2 = _plane.roll * 0.00050; } } torque += torque2; if (dYaw != (torque * loopTime)) dYaw += torque * 1.5; } // Flight model. Apply Rotations. // Transform pitch into components of yaw and pitch based on roll. _plane.roll += dRoll; _plane.yaw += dYaw; _plane.pitch += (dPitch * cos(_plane.roll)); _plane.yaw += -(dPitch * sin(_plane.roll)); if (_plane.roll > 180) { _plane.roll = -180 + (_plane.roll - 180); } else { if (_plane.roll < -180) _plane.roll = 180 + (_plane.roll + 180); } if (_plane.yaw > 180) { _plane.yaw = -180 + (_plane.yaw - 180); } else { if (_plane.yaw < -180) _plane.yaw = 180 + (_plane.yaw + 180); } // Handle special case when aircraft pitch passes vertical. if ((_plane.pitch > 90) || (_plane.pitch < -90)) { if (_plane.roll >= 0) { _plane.roll -= 180; } else { if (_plane.roll < 0) _plane.roll += 180; } if (_plane.yaw >= 0) { _plane.yaw -= 180; } else { if (_plane.yaw < 0) _plane.yaw += 180; } if (_plane.pitch > 0) { _plane.pitch = (180 - _plane.pitch); } else { if (_plane.pitch < 0) _plane.pitch = (-180 - _plane.pitch); } } // Dampen everything out to 0 if they get close enough. if ((_plane.pitch > -0.5) && (_plane.pitch < 0.5)) _plane.pitch = 0; if ((_plane.roll > -0.5) && (_plane.roll < 0.5)) _plane.roll = 0; if ((_plane.yaw > -0.5) && (_plane.yaw < 0.5)) _plane.yaw = 0; } //#define SEGMENT_FLIGHT_MODEL_3 void moveAircraft(void) { // Calculate new aircraft position. Each coordinate is 1 foot in 3D space. tmpX = 0; tmpY = 0; tmpZ = deltaZ; // Order of these points is significant. // Rotate in Z. newX = (tmpX * cos(_plane.roll)) - (tmpY * sin(_plane.roll)); newY = (tmpX * sin(_plane.roll)) + (tmpY * cos(_plane.roll)); tmpX = newX; tmpY = newY; // Rotate in X; _plane.efAOF = Degs(_plane.efAOF); newY = (tmpY * cos(_plane.efAOF)) - (tmpZ * sin(_plane.efAOF)); newZ = (tmpY * sin(_plane.efAOF)) + (tmpZ * cos(_plane.efAOF)); tmpY = newY; tmpZ = newZ; // Rotate in X; newX = (tmpZ * sin(_plane.yaw)) + (tmpX * cos(_plane.yaw)); newZ = (tmpZ * cos(_plane.yaw)) - (tmpX * sin(_plane.yaw)); tmpX = newX; tmpZ = newZ; // Translate rotated point back to where it should be // relative to it's last position. collectX += newX; if ((collectX > 1) || (collectX < -1)) { _plane.x -= collectX; collectX = 0; } collectY += newY; if ((collectY > 1) || (collectY < -1)) { _plane.y -= collectY; collectY = 0; } collectZ += newZ; if ((collectY > 1) || (collectY < -1)) { _plane.z += collectZ; collectZ = 0; } // Are we flying? if ((!_plane.airborne) && (-_plane.y)) _plane.airborne = true; } #endif #define SEGMENT_MAIN void runSimulation(void) { bool pageZero; // Initialize airplane. // memset(&_plane, 0, sizeof(airplaneT)); // Which page is visible? pageZero = true; // Draw it! while (true) { // Flip pages. if (pageZero) { // Looking at bitmap 0. pageZero = false; bitmapSetActive(1); bitmapSetVisible(0, true); bitmapSetVisible(1, false); _drawlist = DRAWLIST_P1; } else { // Looking at bitmap 1. pageZero = true; bitmapSetActive(0); bitmapSetVisible(1, true); bitmapSetVisible(0, false); _drawlist = DRAWLIST_P0; } #if 0 // Erase old scene. bitmapSetColor(0); _useColor = false; //textPrint("a23d2Draw()\n"); a23d2Draw(); #else graphicsWaitVerticalBlank(); dmaClear(pageZero ? 0x6c000 : 0x58000, 0x12c00, 0); #endif // Draw new scene. //textPrint("a23d2Render()\n"); a23d2Render(); _useColor = true; //textPrint("a23d2Draw()\n"); a23d2Draw(); #if 0 // Update player input. getInput(); // Update aircraft control state. if ((_gamepad & JOY_BUTTON_1) || (_gamepad & JOY_BUTTON_2) || (_gamepad & JOY_BUTTON_3)) { // Modified input with button down. if ((_gamepad & JOY_UP) && (_plane.throttle > -15)) _plane.throttle++; if ((_gamepad & JOY_DOWN) && (_plane.throttle < 15)) _plane.throttle--; if (_gamepad & JOY_RIGHT) _plane.brake = !_plane.brake; } else { // No button pressed. if ((_gamepad & JOY_UP) && (_plane.elevator > -15)) _plane.elevator--; if ((_gamepad & JOY_DOWN) && (_plane.elevator < 15)) _plane.elevator++; if ((_gamepad & JOY_LEFT) && (_plane.aileron > -15)) _plane.aileron--; if ((_gamepad & JOY_RIGHT) && (_plane.aileron < 15)) _plane.aileron++; } // "Coordinated" flight. _plane.rudder = _plane.aileron; // Do the actual flying! lightPlane(); lightPlane2(); moveAircraft(); #endif // Move camera. //_camera->p += 1; // Change pitch angle. //_camera->b += 2; // Change bank angle. _camera->h += 1; // Change heading angle. } } int main(int argc, char *argv[]) { byte c; byte ega[16][3] = { { 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0xaa }, { 0x00, 0xaa, 0x00 }, { 0x00, 0xaa, 0xaa }, { 0xaa, 0x00, 0x00 }, { 0xaa, 0x00, 0xaa }, { 0xaa, 0x55, 0x00 }, { 0xaa, 0xaa, 0xaa }, { 0x55, 0x55, 0x55 }, { 0x55, 0x55, 0xff }, { 0x55, 0xff, 0x55 }, { 0x55, 0xff, 0xff }, { 0xff, 0x55, 0x55 }, { 0xff, 0x55, 0xff }, { 0xff, 0xff, 0x55 }, { 0xff, 0xff, 0xff } }; // Load EGA palette into CLUT0. for (c=0; c<16; c++) graphicsDefineColor(0, c, ega[c][0], ega[c][1], ega[c][2]); //textPrint("a23d2Init()\n"); a23d2Init(); _camera->x = 5000; _camera->y = 1000; _camera->z = 5000; _camera->p = 25; // Set up bitmap planes. for (c=0; c<2; c++) { // Clear screen. bitmapSetActive(c); bitmapSetColor(0); bitmapClear(); // Control Panel. bitmapSetColor(8); bitmapLine(0, 194, 319, 194); // Leaves 45 pixels at the bottom below this line. } bitmapSetVisible(0, true); bitmapSetVisible(1, false); //textPrint("runSimulation()\n"); runSimulation(); return 0; }