|
|
1.1 root 1: /*-
2: * Copyright (c) 1990 William Jolitz.
3: * Copyright (c) 1991 The Regents of the University of California.
4: * All rights reserved.
5: *
6: * Redistribution and use in source and binary forms, with or without
7: * modification, are permitted provided that the following conditions
8: * are met:
9: * 1. Redistributions of source code must retain the above copyright
10: * notice, this list of conditions and the following disclaimer.
11: * 2. Redistributions in binary form must reproduce the above copyright
12: * notice, this list of conditions and the following disclaimer in the
13: * documentation and/or other materials provided with the distribution.
14: * 3. All advertising materials mentioning features or use of this software
15: * must display the following acknowledgement:
16: * This product includes software developed by the University of
17: * California, Berkeley and its contributors.
18: * 4. Neither the name of the University nor the names of its contributors
19: * may be used to endorse or promote products derived from this software
20: * without specific prior written permission.
21: *
22: * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
23: * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25: * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
26: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28: * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30: * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31: * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32: * SUCH DAMAGE.
33: *
1.1.1.5 ! root 34: * from: @(#)npx.c 7.2 (Berkeley) 5/12/91
! 35: * npx.c,v 1.7 1993/06/14 07:06:49 cgd Exp
1.1 root 36: */
37: #include "npx.h"
38: #if NNPX > 0
39:
40: #include "param.h"
41: #include "systm.h"
42: #include "conf.h"
43: #include "file.h"
44: #include "proc.h"
45: #include "machine/cpu.h"
46: #include "machine/pcb.h"
47: #include "machine/trap.h"
48: #include "ioctl.h"
1.1.1.5 ! root 49: #include "i386/isa/icu.h"
1.1 root 50: #include "machine/specialreg.h"
51: #include "i386/isa/isa_device.h"
1.1.1.5 ! root 52: #include "i386/isa/isa.h"
! 53:
1.1 root 54: /*
55: * 387 and 287 Numeric Coprocessor Extension (NPX) Driver.
56: */
57:
1.1.1.5 ! root 58: #ifdef __GNUC__
! 59:
! 60: #define disable_intr() __asm("cli")
! 61: #define enable_intr() __asm("sti")
! 62: #define fldcw(addr) __asm("fldcw %0" : : "m" (*addr))
! 63: #define fnclex() __asm("fnclex")
! 64: #define fninit() __asm("fninit")
! 65: #define fnsave(addr) __asm("fnsave %0" : "=m" (*addr) : "0" (*addr))
! 66: #define fnstcw(addr) __asm("fnstcw %0" : "=m" (*addr) : "0" (*addr))
! 67: #define fnstsw(addr) __asm("fnstsw %0" : "=m" (*addr) : "0" (*addr))
! 68: #define fp_divide_by_0() __asm("fldz; fld1; fdiv %st,%st(1); fwait")
! 69: #define frstor(addr) __asm("frstor %0" : : "m" (*addr))
! 70: #define fwait() __asm("fwait")
! 71: #define read_eflags() ({u_long ef; \
! 72: __asm("pushf; popl %0" : "=a" (ef)); \
! 73: ef; })
! 74: #define start_emulating() __asm("smsw %%ax; orb %0,%%al; lmsw %%ax" \
! 75: : : "n" (CR0_TS) : "ax")
! 76: #define stop_emulating() __asm("clts")
! 77: #define write_eflags(ef) __asm("pushl %0; popf" : : "a" ((u_long) ef))
! 78:
! 79: #else /* not __GNUC__ */
! 80:
! 81: void disable_intr __P((void));
! 82: void enable_intr __P((void));
! 83: void fldcw __P((caddr_t addr));
! 84: void fnclex __P((void));
! 85: void fninit __P((void));
! 86: void fnsave __P((caddr_t addr));
! 87: void fnstcw __P((caddr_t addr));
! 88: void fnstsw __P((caddr_t addr));
! 89: void fp_divide_by_0 __P((void));
! 90: void frstor __P((caddr_t addr));
! 91: void fwait __P((void));
! 92: u_long read_eflags __P((void));
! 93: void start_emulating __P((void));
! 94: void stop_emulating __P((void));
! 95: void write_eflags __P((u_long ef));
! 96:
! 97: #endif /* __GNUC__ */
! 98:
! 99: typedef u_char bool_t;
! 100:
! 101: extern struct gate_descriptor idt[];
! 102:
! 103: int npxdna __P((void));
! 104: void npxexit __P((struct proc *p));
! 105: void npxinit __P((u_int control));
! 106: void npxintr __P((struct intrframe frame));
! 107: void npxsave __P((struct save87 *addr));
! 108: static int npxattach __P((struct isa_device *dvp));
! 109: static int npxprobe __P((struct isa_device *dvp));
! 110: static int npxprobe1 __P((struct isa_device *dvp));
! 111:
1.1 root 112: struct isa_driver npxdriver = {
113: npxprobe, npxattach, "npx",
114: };
115:
1.1.1.5 ! root 116: u_int npx0mask;
! 117: struct proc *npxproc;
! 118:
! 119: static bool_t npx_ex16;
! 120: static bool_t npx_exists;
! 121: static struct gate_descriptor npx_idt_probeintr;
! 122: static int npx_intrno;
! 123: static volatile u_int npx_intrs_while_probing;
! 124: static bool_t npx_irq13;
! 125: static volatile u_int npx_traps_while_probing;
1.1 root 126:
127: /*
1.1.1.5 ! root 128: * Special interrupt handlers. Someday intr0-intr15 will be used to count
! 129: * interrupts. We'll still need a special exception 16 handler. The busy
! 130: * latch stuff in probintr() can be moved to npxprobe().
1.1 root 131: */
1.1.1.5 ! root 132: void probeintr(void);
! 133: asm ( \
! 134: ".text;" \
! 135: "_probeintr:;" \
! 136: "ss;" \
! 137: "incl _npx_intrs_while_probing;" \
! 138: "pushl %eax;" \
! 139: "movb $0x20,%al;" /* EOI (asm in strings loses cpp features) */ \
! 140: "outb %al,$0xa0;" /* IO_ICU2 */ \
! 141: "outb %al,$0x20;" /* IO_ICU1 */ \
! 142: "movb $0,%al;" \
! 143: "outb %al,$0xf0;" /* clear BUSY# latch */ \
! 144: "popl %eax;" \
! 145: "iret" \
! 146: );
! 147:
! 148: void probetrap(void);
! 149: asm
! 150: ("
! 151: .text
! 152: _probetrap:
! 153: ss
! 154: incl _npx_traps_while_probing
! 155: fnclex
! 156: iret
! 157: ");
! 158:
! 159: /*
! 160: * Probe routine. Initialize cr0 to give correct behaviour for [f]wait
! 161: * whether the device exists or not (XXX should be elsewhere). Set flags
! 162: * to tell npxattach() what to do. Modify device struct if npx doesn't
! 163: * need to use interrupts. Return 1 if device exists.
! 164: */
! 165: static int
1.1 root 166: npxprobe(dvp)
167: struct isa_device *dvp;
1.1.1.5 ! root 168: {
! 169: int result;
! 170: u_long save_eflags;
! 171: u_char save_icu1_mask;
! 172: u_char save_icu2_mask;
! 173: struct gate_descriptor save_idt_npxintr;
! 174: struct gate_descriptor save_idt_npxtrap;
! 175: /*
! 176: * This routine is now just a wrapper for npxprobe1(), to install
! 177: * special npx interrupt and trap handlers, to enable npx interrupts
! 178: * and to disable other interrupts. Someday isa_configure() will
! 179: * install suitable handlers and run with interrupts enabled so we
! 180: * won't need to do so much here.
! 181: */
! 182: npx_intrno = NRSVIDT + ffs(dvp->id_irq) - 1;
! 183: save_eflags = read_eflags();
! 184: disable_intr();
! 185: save_icu1_mask = inb(IO_ICU1 + 1);
! 186: save_icu2_mask = inb(IO_ICU2 + 1);
! 187: save_idt_npxintr = idt[npx_intrno];
! 188: save_idt_npxtrap = idt[16];
! 189: outb(IO_ICU1 + 1, ~(IRQ_SLAVE | dvp->id_irq));
! 190: outb(IO_ICU2 + 1, ~(dvp->id_irq >> 8));
! 191: setidt(16, probetrap, SDT_SYS386TGT, SEL_KPL);
! 192: setidt(npx_intrno, probeintr, SDT_SYS386IGT, SEL_KPL);
! 193: npx_idt_probeintr = idt[npx_intrno];
! 194: enable_intr();
! 195: result = npxprobe1(dvp);
! 196: disable_intr();
! 197: outb(IO_ICU1 + 1, save_icu1_mask);
! 198: outb(IO_ICU2 + 1, save_icu2_mask);
! 199: idt[npx_intrno] = save_idt_npxintr;
! 200: idt[16] = save_idt_npxtrap;
! 201: write_eflags(save_eflags);
! 202: return (result);
! 203: }
1.1 root 204:
1.1.1.5 ! root 205: static int
! 206: npxprobe1(dvp)
! 207: struct isa_device *dvp;
! 208: {
! 209: int control;
! 210: int status;
1.1 root 211: #ifdef lint
212: npxintr();
213: #endif
1.1.1.5 ! root 214: /*
! 215: * Partially reset the coprocessor, if any. Some BIOS's don't reset
! 216: * it after a warm boot.
! 217: */
! 218: outb(0xf1, 0); /* full reset on some systems, NOP on others */
! 219: outb(0xf0, 0); /* clear BUSY# latch */
! 220: /*
! 221: * Prepare to trap all ESC (i.e., NPX) instructions and all WAIT
! 222: * instructions. We must set the CR0_MP bit and use the CR0_TS
! 223: * bit to control the trap, because setting the CR0_EM bit does
! 224: * not cause WAIT instructions to trap. It's important to trap
! 225: * WAIT instructions - otherwise the "wait" variants of no-wait
! 226: * control instructions would degenerate to the "no-wait" variants
! 227: * after FP context switches but work correctly otherwise. It's
! 228: * particularly important to trap WAITs when there is no NPX -
! 229: * otherwise the "wait" variants would always degenerate.
! 230: *
! 231: * Try setting CR0_NE to get correct error reporting on 486DX's.
! 232: * Setting it should fail or do nothing on lesser processors.
! 233: */
! 234: load_cr0(rcr0() | CR0_MP | CR0_NE);
! 235: /*
! 236: * But don't trap while we're probing.
! 237: */
! 238: stop_emulating();
! 239: /*
! 240: * Finish resetting the coprocessor, if any. If there is an error
! 241: * pending, then we may get a bogus IRQ13, but probeintr() will handle
! 242: * it OK. Bogus halts have never been observed, but we enabled
! 243: * IRQ13 and cleared the BUSY# latch early to handle them anyway.
! 244: */
! 245: fninit();
! 246: DELAY(1000); /* wait for any IRQ13 (fwait might hang) */
! 247: #ifdef DIAGNOSTIC
! 248: if (npx_intrs_while_probing != 0)
! 249: printf("fninit caused %u bogus npx interrupt(s)\n",
! 250: npx_intrs_while_probing);
! 251: if (npx_traps_while_probing != 0)
! 252: printf("fninit caused %u bogus npx trap(s)\n",
! 253: npx_traps_while_probing);
! 254: #endif
! 255: /*
! 256: * Check for a status of mostly zero.
! 257: */
! 258: status = 0x5a5a;
! 259: fnstsw(&status);
! 260: if ((status & 0xb8ff) == 0) {
! 261: /*
! 262: * Good, now check for a proper control word.
! 263: */
! 264: control = 0x5a5a;
! 265: fnstcw(&control);
! 266: if ((control & 0x1f3f) == 0x033f) {
! 267: npx_exists = 1;
! 268: /*
! 269: * We have an npx, now divide by 0 to see if exception
! 270: * 16 works.
! 271: */
! 272: control &= ~(1 << 2); /* enable divide by 0 trap */
! 273: fldcw(&control);
! 274: npx_traps_while_probing = npx_intrs_while_probing = 0;
! 275: fp_divide_by_0();
! 276: if (npx_traps_while_probing != 0) {
! 277: /*
! 278: * Good, exception 16 works.
! 279: */
! 280: npx_ex16 = 1;
! 281: dvp->id_irq = 0; /* zap the interrupt */
! 282: return 16;
! 283: }
! 284: if (npx_intrs_while_probing != 0) {
! 285: /*
! 286: * Bad, we are stuck with IRQ13.
! 287: */
! 288: npx_irq13 = 1;
! 289: npx0mask = dvp->id_irq; /* npxattach too late */
! 290: return 16;
! 291: }
! 292: /*
! 293: * Worse, even IRQ13 is broken. Use emulator.
! 294: */
1.1 root 295: }
296: }
1.1.1.5 ! root 297: /*
! 298: * Probe failed, but we want to get to npxattach to initialize the
! 299: * emulator and say that it has been installed. XXX handle devices
! 300: * that aren't really devices better.
! 301: */
! 302: dvp->id_irq = 0;
! 303: return 16;
1.1 root 304: }
305:
306: /*
307: * Attach routine - announce which it is, and wire into system
308: */
1.1.1.5 ! root 309: int
1.1 root 310: npxattach(dvp)
311: struct isa_device *dvp;
312: {
1.1.1.5 ! root 313: if (npx_ex16)
! 314: printf("npx%d: using exception 16\n", dvp->id_unit);
! 315: else if (npx_irq13)
! 316: ;
! 317: else {
! 318: #ifdef MATH_EMULATE
! 319: if (npx_exists)
! 320: printf("npx%d: error reporting broken, using emulator\n",
! 321: dvp->id_unit);
! 322: else
! 323: printf("npx%d: emulator\n", dvp->id_unit);
1.1.1.4 root 324: #else
1.1.1.5 ! root 325: panic("npxattach: no math emulator in kernel!");
1.1.1.4 root 326: #endif
1.1.1.3 root 327: }
1.1.1.5 ! root 328: npxinit(__INITIAL_NPXCW__);
! 329: return (1);
1.1 root 330: }
331:
332: /*
1.1.1.5 ! root 333: * Initialize floating point unit.
1.1 root 334: */
1.1.1.5 ! root 335: void
! 336: npxinit(control)
! 337: u_int control;
! 338: {
! 339: struct save87 dummy;
1.1 root 340:
1.1.1.5 ! root 341: if (!npx_exists)
! 342: return;
! 343: /*
! 344: * fninit has the same h/w bugs as fnsave. Use the detoxified
! 345: * fnsave to throw away any junk in the fpu. fnsave initializes
! 346: * the fpu and sets npxproc = NULL as important side effects.
! 347: */
! 348: npxsave(&dummy);
! 349: stop_emulating();
! 350: fldcw(&control);
! 351: if (curpcb != NULL)
! 352: fnsave(&curpcb->pcb_savefpu);
! 353: start_emulating();
1.1 root 354: }
355:
356: /*
1.1.1.5 ! root 357: * Free coprocessor (if we have it).
1.1 root 358: */
1.1.1.5 ! root 359: void
! 360: npxexit(p)
! 361: struct proc *p;
! 362: {
! 363: if (p == npxproc) {
! 364: start_emulating();
! 365: npxproc = NULL;
! 366: }
1.1 root 367: }
368:
369: /*
1.1.1.5 ! root 370: * Record the FPU state and reinitialize it all except for the control word.
! 371: * Then generate a SIGFPE.
! 372: *
! 373: * Reinitializing the state allows naive SIGFPE handlers to longjmp without
! 374: * doing any fixups.
! 375: *
! 376: * XXX there is currently no way to pass the full error state to signal
! 377: * handlers, and if this is a nested interrupt there is no way to pass even
! 378: * a status code! So there is no way to have a non-naive SIGFPE handler. At
! 379: * best a handler could do an fninit followed by an fldcw of a static value.
! 380: * fnclex would be of little use because it would leave junk on the FPU stack.
! 381: * Returning from the handler would be even less safe than usual because
! 382: * IRQ13 exception handling makes exceptions even less precise than usual.
1.1 root 383: */
1.1.1.5 ! root 384: void
! 385: npxintr(frame)
! 386: struct intrframe frame;
! 387: {
1.1.1.3 root 388: int code;
1.1 root 389:
1.1.1.5 ! root 390: if (npxproc == NULL || !npx_exists) {
! 391: /* XXX no %p in stand/printf.c. Cast to quiet gcc -Wall. */
! 392: printf("npxintr: npxproc = %lx, curproc = %lx, npx_exists = %d\n",
! 393: (u_long) npxproc, (u_long) curproc, npx_exists);
! 394: panic("npxintr from nowhere");
! 395: }
! 396: if (npxproc != curproc) {
! 397: printf("npxintr: npxproc = %lx, curproc = %lx, npx_exists = %d\n",
! 398: (u_long) npxproc, (u_long) curproc, npx_exists);
! 399: panic("npxintr from non-current process");
! 400: }
! 401: /*
! 402: * Save state. This does an implied fninit. It had better not halt
! 403: * the cpu or we'll hang.
! 404: */
! 405: outb(0xf0, 0);
! 406: fnsave(&curpcb->pcb_savefpu);
! 407: fwait();
! 408: /*
! 409: * Restore control word (was clobbered by fnsave).
! 410: */
! 411: fldcw(&curpcb->pcb_savefpu.sv_env.en_cw);
! 412: fwait();
! 413: /*
! 414: * Remember the exception status word and tag word. The current
! 415: * (almost fninit'ed) fpu state is in the fpu and the exception
! 416: * state just saved will soon be junk. However, the implied fninit
! 417: * doesn't change the error pointers or register contents, and we
! 418: * preserved the control word and will copy the status and tag
! 419: * words, so the complete exception state can be recovered.
! 420: */
! 421: curpcb->pcb_savefpu.sv_ex_sw = curpcb->pcb_savefpu.sv_env.en_sw;
! 422: curpcb->pcb_savefpu.sv_ex_tw = curpcb->pcb_savefpu.sv_env.en_tw;
! 423:
! 424: /*
! 425: * Pass exception to process.
! 426: */
! 427: if (ISPL(frame.if_cs) == SEL_UPL) {
! 428: /*
! 429: * Interrupt is essentially a trap, so we can afford to call
! 430: * the SIGFPE handler (if any) as soon as the interrupt
! 431: * returns.
! 432: *
! 433: * XXX little or nothing is gained from this, and plenty is
! 434: * lost - the interrupt frame has to contain the trap frame
! 435: * (this is otherwise only necessary for the rescheduling trap
! 436: * in doreti, and the frame for that could easily be set up
! 437: * just before it is used).
! 438: */
! 439: curproc->p_regs = (int *)&frame.if_es;
! 440: curpcb->pcb_flags |= FM_TRAP; /* used by sendsig */
1.1 root 441: #ifdef notyet
1.1.1.5 ! root 442: /*
! 443: * Encode the appropriate code for detailed information on
! 444: * this exception.
! 445: */
! 446: code = XXX_ENCODE(curpcb->pcb_savefpu.sv_ex_sw);
1.1.1.3 root 447: #else
1.1.1.5 ! root 448: code = 0; /* XXX */
1.1 root 449: #endif
1.1.1.3 root 450: trapsignal(curproc, SIGFPE, code);
1.1.1.5 ! root 451: curpcb->pcb_flags &= ~FM_TRAP;
1.1.1.3 root 452: } else {
1.1.1.5 ! root 453: /*
! 454: * Nested interrupt. These losers occur when:
! 455: * o an IRQ13 is bogusly generated at a bogus time, e.g.:
! 456: * o immediately after an fnsave or frstor of an
! 457: * error state.
! 458: * o a couple of 386 instructions after
! 459: * "fstpl _memvar" causes a stack overflow.
! 460: * These are especially nasty when combined with a
! 461: * trace trap.
! 462: * o an IRQ13 occurs at the same time as another higher-
! 463: * priority interrupt.
! 464: *
! 465: * Treat them like a true async interrupt.
! 466: */
1.1.1.3 root 467: psignal(npxproc, SIGFPE);
468: }
1.1 root 469: }
470:
471: /*
472: * Implement device not available (DNA) exception
1.1.1.5 ! root 473: *
! 474: * It would be better to switch FP context here (only). This would require
! 475: * saving the state in the proc table instead of in the pcb.
1.1 root 476: */
1.1.1.5 ! root 477: int
! 478: npxdna()
! 479: {
! 480: if (!npx_exists)
! 481: return (0);
! 482: if (npxproc != NULL) {
! 483: printf("npxdna: npxproc = %lx, curproc = %lx\n",
! 484: (u_long) npxproc, (u_long) curproc);
! 485: panic("npxdna");
! 486: }
! 487: stop_emulating();
! 488: /*
! 489: * Record new context early in case frstor causes an IRQ13.
! 490: */
1.1.1.2 root 491: npxproc = curproc;
1.1.1.5 ! root 492: /*
! 493: * The following frstor may cause an IRQ13 when the state being
! 494: * restored has a pending error. The error will appear to have been
! 495: * triggered by the current (npx) user instruction even when that
! 496: * instruction is a no-wait instruction that should not trigger an
! 497: * error (e.g., fnclex). On at least one 486 system all of the
! 498: * no-wait instructions are broken the same as frstor, so our
! 499: * treatment does not amplify the breakage. On at least one
! 500: * 386/Cyrix 387 system, fnclex works correctly while frstor and
! 501: * fnsave are broken, so our treatment breaks fnclex if it is the
! 502: * first FPU instruction after a context switch.
! 503: */
! 504: frstor(&curpcb->pcb_savefpu);
! 505:
1.1.1.2 root 506: return (1);
1.1 root 507: }
1.1.1.5 ! root 508:
! 509: /*
! 510: * Wrapper for fnsave instruction to handle h/w bugs. If there is an error
! 511: * pending, then fnsave generates a bogus IRQ13 on some systems. Force
! 512: * any IRQ13 to be handled immediately, and then ignore it. This routine is
! 513: * often called at splhigh so it must not use many system services. In
! 514: * particular, it's much easier to install a special handler than to
! 515: * guarantee that it's safe to use npxintr() and its supporting code.
! 516: */
! 517: void
! 518: npxsave(addr)
! 519: struct save87 *addr;
! 520: {
! 521: u_char icu1_mask;
! 522: u_char icu2_mask;
! 523: u_char old_icu1_mask;
! 524: u_char old_icu2_mask;
! 525: struct gate_descriptor save_idt_npxintr;
! 526:
! 527: disable_intr();
! 528: old_icu1_mask = inb(IO_ICU1 + 1);
! 529: old_icu2_mask = inb(IO_ICU2 + 1);
! 530: save_idt_npxintr = idt[npx_intrno];
! 531: outb(IO_ICU1 + 1, old_icu1_mask & ~(IRQ_SLAVE | npx0mask));
! 532: outb(IO_ICU2 + 1, old_icu2_mask & ~(npx0mask >> 8));
! 533: idt[npx_intrno] = npx_idt_probeintr;
! 534: enable_intr();
! 535: stop_emulating();
! 536: fnsave(addr);
! 537: fwait();
! 538: start_emulating();
! 539: npxproc = NULL;
! 540: disable_intr();
! 541: icu1_mask = inb(IO_ICU1 + 1); /* masks may have changed */
! 542: icu2_mask = inb(IO_ICU2 + 1);
! 543: outb(IO_ICU1 + 1,
! 544: (icu1_mask & ~npx0mask) | (old_icu1_mask & npx0mask));
! 545: outb(IO_ICU2 + 1,
! 546: (icu2_mask & ~(npx0mask >> 8))
! 547: | (old_icu2_mask & (npx0mask >> 8)));
! 548: idt[npx_intrno] = save_idt_npxintr;
! 549: enable_intr(); /* back to usual state */
! 550: }
! 551:
! 552: #endif /* NNPX > 0 */
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.