|
|
1.1 ! root 1: /**************************************************************************** ! 2: * ! 3: * VBE 2.0 Linear Framebuffer Profiler ! 4: * By Kendall Bennett and Brian Hook ! 5: * ! 6: * Filename: LFBPROF.C ! 7: * Language: ANSI C ! 8: * Environment: Watcom C/C++ 10.0a with DOS4GW ! 9: * ! 10: * Description: Simple program to profile the speed of screen clearing ! 11: * and full screen BitBlt operations using a VESA VBE 2.0 ! 12: * linear framebuffer from 32 bit protected mode. ! 13: * ! 14: * For simplicity, this program only supports 256 color ! 15: * SuperVGA video modes that support a linear framebuffer. ! 16: * ! 17: * ! 18: * 2002/02/18: Jeroen Janssen <japj at xs4all dot nl> ! 19: * - fixed unsigned short for mode list (-1 != 0xffff otherwise) ! 20: * - fixed LfbMapRealPointer macro mask problem (some modes were skipped) ! 21: * ! 22: ****************************************************************************/ ! 23: ! 24: #include <stdio.h> ! 25: #include <stdlib.h> ! 26: #include <string.h> ! 27: #include <conio.h> ! 28: #include <dos.h> ! 29: #include "lfbprof.h" ! 30: ! 31: /*---------------------------- Global Variables ---------------------------*/ ! 32: ! 33: int VESABuf_len = 1024; /* Length of VESABuf */ ! 34: int VESABuf_sel = 0; /* Selector for VESABuf */ ! 35: int VESABuf_rseg; /* Real mode segment of VESABuf */ ! 36: unsigned short modeList[50]; /* List of available VBE modes */ ! 37: float clearsPerSec; /* Number of clears per second */ ! 38: float clearsMbPerSec; /* Memory transfer for clears */ ! 39: float bitBltsPerSec; /* Number of BitBlt's per second */ ! 40: float bitBltsMbPerSec; /* Memory transfer for bitblt's */ ! 41: int xres,yres; /* Video mode resolution */ ! 42: int bytesperline; /* Bytes per scanline for mode */ ! 43: long imageSize; /* Length of the video image */ ! 44: char *LFBPtr; /* Pointer to linear framebuffer */ ! 45: ! 46: /*------------------------- DPMI interface routines -----------------------*/ ! 47: ! 48: void DPMI_allocRealSeg(int size,int *sel,int *r_seg) ! 49: /**************************************************************************** ! 50: * ! 51: * Function: DPMI_allocRealSeg ! 52: * Parameters: size - Size of memory block to allocate ! 53: * sel - Place to return protected mode selector ! 54: * r_seg - Place to return real mode segment ! 55: * ! 56: * Description: Allocates a block of real mode memory using DPMI services. ! 57: * This routine returns both a protected mode selector and ! 58: * real mode segment for accessing the memory block. ! 59: * ! 60: ****************************************************************************/ ! 61: { ! 62: union REGS r; ! 63: ! 64: r.w.ax = 0x100; /* DPMI allocate DOS memory */ ! 65: r.w.bx = (size + 0xF) >> 4; /* number of paragraphs */ ! 66: int386(0x31, &r, &r); ! 67: if (r.w.cflag) ! 68: FatalError("DPMI_allocRealSeg failed!"); ! 69: *sel = r.w.dx; /* Protected mode selector */ ! 70: *r_seg = r.w.ax; /* Real mode segment */ ! 71: } ! 72: ! 73: void DPMI_freeRealSeg(unsigned sel) ! 74: /**************************************************************************** ! 75: * ! 76: * Function: DPMI_allocRealSeg ! 77: * Parameters: sel - Protected mode selector of block to free ! 78: * ! 79: * Description: Frees a block of real mode memory. ! 80: * ! 81: ****************************************************************************/ ! 82: { ! 83: union REGS r; ! 84: ! 85: r.w.ax = 0x101; /* DPMI free DOS memory */ ! 86: r.w.dx = sel; /* DX := selector from 0x100 */ ! 87: int386(0x31, &r, &r); ! 88: } ! 89: ! 90: typedef struct { ! 91: long edi; ! 92: long esi; ! 93: long ebp; ! 94: long reserved; ! 95: long ebx; ! 96: long edx; ! 97: long ecx; ! 98: long eax; ! 99: short flags; ! 100: short es,ds,fs,gs,ip,cs,sp,ss; ! 101: } _RMREGS; ! 102: ! 103: #define IN(reg) rmregs.e##reg = in->x.reg ! 104: #define OUT(reg) out->x.reg = rmregs.e##reg ! 105: ! 106: int DPMI_int86(int intno, RMREGS *in, RMREGS *out) ! 107: /**************************************************************************** ! 108: * ! 109: * Function: DPMI_int86 ! 110: * Parameters: intno - Interrupt number to issue ! 111: * in - Pointer to structure for input registers ! 112: * out - Pointer to structure for output registers ! 113: * Returns: Value returned by interrupt in AX ! 114: * ! 115: * Description: Issues a real mode interrupt using DPMI services. ! 116: * ! 117: ****************************************************************************/ ! 118: { ! 119: _RMREGS rmregs; ! 120: union REGS r; ! 121: struct SREGS sr; ! 122: ! 123: memset(&rmregs, 0, sizeof(rmregs)); ! 124: IN(ax); IN(bx); IN(cx); IN(dx); IN(si); IN(di); ! 125: ! 126: segread(&sr); ! 127: r.w.ax = 0x300; /* DPMI issue real interrupt */ ! 128: r.h.bl = intno; ! 129: r.h.bh = 0; ! 130: r.w.cx = 0; ! 131: sr.es = sr.ds; ! 132: r.x.edi = (unsigned)&rmregs; ! 133: int386x(0x31, &r, &r, &sr); /* Issue the interrupt */ ! 134: ! 135: OUT(ax); OUT(bx); OUT(cx); OUT(dx); OUT(si); OUT(di); ! 136: out->x.cflag = rmregs.flags & 0x1; ! 137: return out->x.ax; ! 138: } ! 139: ! 140: int DPMI_int86x(int intno, RMREGS *in, RMREGS *out, RMSREGS *sregs) ! 141: /**************************************************************************** ! 142: * ! 143: * Function: DPMI_int86 ! 144: * Parameters: intno - Interrupt number to issue ! 145: * in - Pointer to structure for input registers ! 146: * out - Pointer to structure for output registers ! 147: * sregs - Values to load into segment registers ! 148: * Returns: Value returned by interrupt in AX ! 149: * ! 150: * Description: Issues a real mode interrupt using DPMI services. ! 151: * ! 152: ****************************************************************************/ ! 153: { ! 154: _RMREGS rmregs; ! 155: union REGS r; ! 156: struct SREGS sr; ! 157: ! 158: memset(&rmregs, 0, sizeof(rmregs)); ! 159: IN(ax); IN(bx); IN(cx); IN(dx); IN(si); IN(di); ! 160: rmregs.es = sregs->es; ! 161: rmregs.ds = sregs->ds; ! 162: ! 163: segread(&sr); ! 164: r.w.ax = 0x300; /* DPMI issue real interrupt */ ! 165: r.h.bl = intno; ! 166: r.h.bh = 0; ! 167: r.w.cx = 0; ! 168: sr.es = sr.ds; ! 169: r.x.edi = (unsigned)&rmregs; ! 170: int386x(0x31, &r, &r, &sr); /* Issue the interrupt */ ! 171: ! 172: OUT(ax); OUT(bx); OUT(cx); OUT(dx); OUT(si); OUT(di); ! 173: sregs->es = rmregs.es; ! 174: sregs->cs = rmregs.cs; ! 175: sregs->ss = rmregs.ss; ! 176: sregs->ds = rmregs.ds; ! 177: out->x.cflag = rmregs.flags & 0x1; ! 178: return out->x.ax; ! 179: } ! 180: ! 181: int DPMI_allocSelector(void) ! 182: /**************************************************************************** ! 183: * ! 184: * Function: DPMI_allocSelector ! 185: * Returns: Newly allocated protected mode selector ! 186: * ! 187: * Description: Allocates a new protected mode selector using DPMI ! 188: * services. This selector has a base address and limit of 0. ! 189: * ! 190: ****************************************************************************/ ! 191: { ! 192: int sel; ! 193: union REGS r; ! 194: ! 195: r.w.ax = 0; /* DPMI allocate selector */ ! 196: r.w.cx = 1; /* Allocate a single selector */ ! 197: int386(0x31, &r, &r); ! 198: if (r.x.cflag) ! 199: FatalError("DPMI_allocSelector() failed!"); ! 200: sel = r.w.ax; ! 201: ! 202: r.w.ax = 9; /* DPMI set access rights */ ! 203: r.w.bx = sel; ! 204: r.w.cx = 0x8092; /* 32 bit page granular */ ! 205: int386(0x31, &r, &r); ! 206: return sel; ! 207: } ! 208: ! 209: long DPMI_mapPhysicalToLinear(long physAddr,long limit) ! 210: /**************************************************************************** ! 211: * ! 212: * Function: DPMI_mapPhysicalToLinear ! 213: * Parameters: physAddr - Physical memory address to map ! 214: * limit - Length-1 of physical memory region to map ! 215: * Returns: Starting linear address for mapped memory ! 216: * ! 217: * Description: Maps a section of physical memory into the linear address ! 218: * space of a process using DPMI calls. Note that this linear ! 219: * address cannot be used directly, but must be used as the ! 220: * base address for a selector. ! 221: * ! 222: ****************************************************************************/ ! 223: { ! 224: union REGS r; ! 225: ! 226: r.w.ax = 0x800; /* DPMI map physical to linear */ ! 227: r.w.bx = physAddr >> 16; ! 228: r.w.cx = physAddr & 0xFFFF; ! 229: r.w.si = limit >> 16; ! 230: r.w.di = limit & 0xFFFF; ! 231: int386(0x31, &r, &r); ! 232: if (r.x.cflag) ! 233: FatalError("DPMI_mapPhysicalToLinear() failed!"); ! 234: return ((long)r.w.bx << 16) + r.w.cx; ! 235: } ! 236: ! 237: void DPMI_setSelectorBase(int sel,long linAddr) ! 238: /**************************************************************************** ! 239: * ! 240: * Function: DPMI_setSelectorBase ! 241: * Parameters: sel - Selector to change base address for ! 242: * linAddr - Linear address used for new base address ! 243: * ! 244: * Description: Sets the base address for the specified selector. ! 245: * ! 246: ****************************************************************************/ ! 247: { ! 248: union REGS r; ! 249: ! 250: r.w.ax = 7; /* DPMI set selector base address */ ! 251: r.w.bx = sel; ! 252: r.w.cx = linAddr >> 16; ! 253: r.w.dx = linAddr & 0xFFFF; ! 254: int386(0x31, &r, &r); ! 255: if (r.x.cflag) ! 256: FatalError("DPMI_setSelectorBase() failed!"); ! 257: } ! 258: ! 259: void DPMI_setSelectorLimit(int sel,long limit) ! 260: /**************************************************************************** ! 261: * ! 262: * Function: DPMI_setSelectorLimit ! 263: * Parameters: sel - Selector to change limit for ! 264: * limit - Limit-1 for the selector ! 265: * ! 266: * Description: Sets the memory limit for the specified selector. ! 267: * ! 268: ****************************************************************************/ ! 269: { ! 270: union REGS r; ! 271: ! 272: r.w.ax = 8; /* DPMI set selector limit */ ! 273: r.w.bx = sel; ! 274: r.w.cx = limit >> 16; ! 275: r.w.dx = limit & 0xFFFF; ! 276: int386(0x31, &r, &r); ! 277: if (r.x.cflag) ! 278: FatalError("DPMI_setSelectorLimit() failed!"); ! 279: } ! 280: ! 281: /*-------------------------- VBE Interface routines -----------------------*/ ! 282: ! 283: void FatalError(char *msg) ! 284: { ! 285: fprintf(stderr,"%s\n", msg); ! 286: exit(1); ! 287: } ! 288: ! 289: static void ExitVBEBuf(void) ! 290: { ! 291: DPMI_freeRealSeg(VESABuf_sel); ! 292: } ! 293: ! 294: void VBE_initRMBuf(void) ! 295: /**************************************************************************** ! 296: * ! 297: * Function: VBE_initRMBuf ! 298: * Description: Initialises the VBE transfer buffer in real mode memory. ! 299: * This routine is called by the VESAVBE module every time ! 300: * it needs to use the transfer buffer, so we simply allocate ! 301: * it once and then return. ! 302: * ! 303: ****************************************************************************/ ! 304: { ! 305: if (!VESABuf_sel) { ! 306: DPMI_allocRealSeg(VESABuf_len, &VESABuf_sel, &VESABuf_rseg); ! 307: atexit(ExitVBEBuf); ! 308: } ! 309: } ! 310: ! 311: void VBE_callESDI(RMREGS *regs, void *buffer, int size) ! 312: /**************************************************************************** ! 313: * ! 314: * Function: VBE_callESDI ! 315: * Parameters: regs - Registers to load when calling VBE ! 316: * buffer - Buffer to copy VBE info block to ! 317: * size - Size of buffer to fill ! 318: * ! 319: * Description: Calls the VESA VBE and passes in a buffer for the VBE to ! 320: * store information in, which is then copied into the users ! 321: * buffer space. This works in protected mode as the buffer ! 322: * passed to the VESA VBE is allocated in conventional ! 323: * memory, and is then copied into the users memory block. ! 324: * ! 325: ****************************************************************************/ ! 326: { ! 327: RMSREGS sregs; ! 328: ! 329: VBE_initRMBuf(); ! 330: sregs.es = VESABuf_rseg; ! 331: regs->x.di = 0; ! 332: _fmemcpy(MK_FP(VESABuf_sel,0),buffer,size); ! 333: DPMI_int86x(0x10, regs, regs, &sregs); ! 334: _fmemcpy(buffer,MK_FP(VESABuf_sel,0),size); ! 335: } ! 336: ! 337: int VBE_detect(void) ! 338: /**************************************************************************** ! 339: * ! 340: * Function: VBE_detect ! 341: * Parameters: vgaInfo - Place to store the VGA information block ! 342: * Returns: VBE version number, or 0 if not detected. ! 343: * ! 344: * Description: Detects if a VESA VBE is out there and functioning ! 345: * correctly. If we detect a VBE interface we return the ! 346: * VGAInfoBlock returned by the VBE and the VBE version number. ! 347: * ! 348: ****************************************************************************/ ! 349: { ! 350: RMREGS regs; ! 351: unsigned short *p1,*p2; ! 352: VBE_vgaInfo vgaInfo; ! 353: ! 354: /* Put 'VBE2' into the signature area so that the VBE 2.0 BIOS knows ! 355: * that we have passed a 512 byte extended block to it, and wish ! 356: * the extended information to be filled in. ! 357: */ ! 358: strncpy(vgaInfo.VESASignature,"VBE2",4); ! 359: ! 360: /* Get the SuperVGA Information block */ ! 361: regs.x.ax = 0x4F00; ! 362: VBE_callESDI(®s, &vgaInfo, sizeof(VBE_vgaInfo)); ! 363: if (regs.x.ax != 0x004F) ! 364: return 0; ! 365: if (strncmp(vgaInfo.VESASignature,"VESA",4) != 0) ! 366: return 0; ! 367: ! 368: /* Now that we have detected a VBE interface, copy the list of available ! 369: * video modes into our local buffer. We *must* copy this mode list, ! 370: * since the VBE will build the mode list in the VBE_vgaInfo buffer ! 371: * that we have passed, so the next call to the VBE will trash the ! 372: * list of modes. ! 373: */ ! 374: printf("videomodeptr %x\n",vgaInfo.VideoModePtr); ! 375: p1 = LfbMapRealPointer(vgaInfo.VideoModePtr); ! 376: p2 = modeList; ! 377: while (*p1 != -1) ! 378: { ! 379: printf("found mode %x\n",*p1); ! 380: *p2++ = *p1++; ! 381: } ! 382: *p2 = -1; ! 383: return vgaInfo.VESAVersion; ! 384: } ! 385: ! 386: int VBE_getModeInfo(int mode,VBE_modeInfo *modeInfo) ! 387: /**************************************************************************** ! 388: * ! 389: * Function: VBE_getModeInfo ! 390: * Parameters: mode - VBE mode to get information for ! 391: * modeInfo - Place to store VBE mode information ! 392: * Returns: 1 on success, 0 if function failed. ! 393: * ! 394: * Description: Obtains information about a specific video mode from the ! 395: * VBE. You should use this function to find the video mode ! 396: * you wish to set, as the new VBE 2.0 mode numbers may be ! 397: * completely arbitrary. ! 398: * ! 399: ****************************************************************************/ ! 400: { ! 401: RMREGS regs; ! 402: ! 403: regs.x.ax = 0x4F01; /* Get mode information */ ! 404: regs.x.cx = mode; ! 405: VBE_callESDI(®s, modeInfo, sizeof(VBE_modeInfo)); ! 406: if (regs.x.ax != 0x004F) ! 407: return 0; ! 408: if ((modeInfo->ModeAttributes & vbeMdAvailable) == 0) ! 409: return 0; ! 410: return 1; ! 411: } ! 412: ! 413: void VBE_setVideoMode(int mode) ! 414: /**************************************************************************** ! 415: * ! 416: * Function: VBE_setVideoMode ! 417: * Parameters: mode - VBE mode number to initialise ! 418: * ! 419: ****************************************************************************/ ! 420: { ! 421: RMREGS regs; ! 422: regs.x.ax = 0x4F02; ! 423: regs.x.bx = mode; ! 424: DPMI_int86(0x10,®s,®s); ! 425: } ! 426: ! 427: /*-------------------- Application specific routines ----------------------*/ ! 428: ! 429: void *GetPtrToLFB(long physAddr) ! 430: /**************************************************************************** ! 431: * ! 432: * Function: GetPtrToLFB ! 433: * Parameters: physAddr - Physical memory address of linear framebuffer ! 434: * Returns: Far pointer to the linear framebuffer memory ! 435: * ! 436: ****************************************************************************/ ! 437: { ! 438: int sel; ! 439: long linAddr,limit = (4096 * 1024) - 1; ! 440: ! 441: // sel = DPMI_allocSelector(); ! 442: linAddr = DPMI_mapPhysicalToLinear(physAddr,limit); ! 443: // DPMI_setSelectorBase(sel,linAddr); ! 444: // DPMI_setSelectorLimit(sel,limit); ! 445: // return MK_FP(sel,0); ! 446: return (void*)linAddr; ! 447: } ! 448: ! 449: void AvailableModes(void) ! 450: /**************************************************************************** ! 451: * ! 452: * Function: AvailableModes ! 453: * ! 454: * Description: Display a list of available LFB mode resolutions. ! 455: * ! 456: ****************************************************************************/ ! 457: { ! 458: unsigned short *p; ! 459: VBE_modeInfo modeInfo; ! 460: ! 461: printf("Usage: LFBPROF <xres> <yres>\n\n"); ! 462: printf("Available 256 color video modes:\n"); ! 463: for (p = modeList; *p != -1; p++) { ! 464: if (VBE_getModeInfo(*p, &modeInfo)) { ! 465: /* Filter out only 8 bit linear framebuffer modes */ ! 466: if ((modeInfo.ModeAttributes & vbeMdLinear) == 0) ! 467: continue; ! 468: if (modeInfo.MemoryModel != vbeMemPK ! 469: || modeInfo.BitsPerPixel != 8 ! 470: || modeInfo.NumberOfPlanes != 1) ! 471: continue; ! 472: printf(" %4d x %4d %d bits per pixel\n", ! 473: modeInfo.XResolution, modeInfo.YResolution, ! 474: modeInfo.BitsPerPixel); ! 475: } ! 476: } ! 477: exit(1); ! 478: } ! 479: ! 480: void InitGraphics(int x,int y) ! 481: /**************************************************************************** ! 482: * ! 483: * Function: InitGraphics ! 484: * Parameters: x,y - Requested video mode resolution ! 485: * ! 486: * Description: Initialise the specified video mode. We search through ! 487: * the list of available video modes for one that matches ! 488: * the resolution and color depth are are looking for. ! 489: * ! 490: ****************************************************************************/ ! 491: { ! 492: unsigned short *p; ! 493: VBE_modeInfo modeInfo; ! 494: printf("InitGraphics\n"); ! 495: ! 496: for (p = modeList; *p != -1; p++) { ! 497: if (VBE_getModeInfo(*p, &modeInfo)) { ! 498: /* Filter out only 8 bit linear framebuffer modes */ ! 499: if ((modeInfo.ModeAttributes & vbeMdLinear) == 0) ! 500: continue; ! 501: if (modeInfo.MemoryModel != vbeMemPK ! 502: || modeInfo.BitsPerPixel != 8 ! 503: || modeInfo.NumberOfPlanes != 1) ! 504: continue; ! 505: if (modeInfo.XResolution != x || modeInfo.YResolution != y) ! 506: continue; ! 507: xres = x; ! 508: yres = y; ! 509: bytesperline = modeInfo.BytesPerScanLine; ! 510: imageSize = bytesperline * yres; ! 511: VBE_setVideoMode(*p | vbeUseLFB); ! 512: LFBPtr = GetPtrToLFB(modeInfo.PhysBasePtr); ! 513: return; ! 514: } ! 515: } ! 516: printf("Valid video mode not found\n"); ! 517: exit(1); ! 518: } ! 519: ! 520: void EndGraphics(void) ! 521: /**************************************************************************** ! 522: * ! 523: * Function: EndGraphics ! 524: * ! 525: * Description: Restores text mode. ! 526: * ! 527: ****************************************************************************/ ! 528: { ! 529: RMREGS regs; ! 530: printf("EndGraphics\n"); ! 531: regs.x.ax = 0x3; ! 532: DPMI_int86(0x10, ®s, ®s); ! 533: } ! 534: ! 535: void ProfileMode(void) ! 536: /**************************************************************************** ! 537: * ! 538: * Function: ProfileMode ! 539: * ! 540: * Description: Profiles framebuffer performance for simple screen clearing ! 541: * and for copying from system memory to video memory (BitBlt). ! 542: * This routine thrashes the CPU cache by cycling through ! 543: * enough system memory buffers to invalidate the entire ! 544: * CPU external cache before re-using the first memory buffer ! 545: * again. ! 546: * ! 547: ****************************************************************************/ ! 548: { ! 549: int i,numClears,numBlts,maxImages; ! 550: long startTicks,endTicks; ! 551: void *image[10],*dst; ! 552: printf("ProfileMode\n"); ! 553: ! 554: /* Profile screen clearing operation */ ! 555: startTicks = LfbGetTicks(); ! 556: numClears = 0; ! 557: while ((LfbGetTicks() - startTicks) < 182) ! 558: LfbMemset(LFBPtr,numClears++,imageSize); ! 559: endTicks = LfbGetTicks(); ! 560: clearsPerSec = numClears / ((endTicks - startTicks) * 0.054925); ! 561: clearsMbPerSec = (clearsPerSec * imageSize) / 1048576.0; ! 562: ! 563: /* Profile system memory to video memory copies */ ! 564: maxImages = ((512 * 1024U) / imageSize) + 2; ! 565: for (i = 0; i < maxImages; i++) { ! 566: image[i] = malloc(imageSize); ! 567: if (image[i] == NULL) ! 568: FatalError("Not enough memory to profile BitBlt!"); ! 569: memset(image[i],i+1,imageSize); ! 570: } ! 571: startTicks = LfbGetTicks(); ! 572: numBlts = 0; ! 573: while ((LfbGetTicks() - startTicks) < 182) ! 574: LfbMemcpy(LFBPtr,image[numBlts++ % maxImages],imageSize); ! 575: endTicks = LfbGetTicks(); ! 576: bitBltsPerSec = numBlts / ((endTicks - startTicks) * 0.054925); ! 577: bitBltsMbPerSec = (bitBltsPerSec * imageSize) / 1048576.0; ! 578: } ! 579: ! 580: void main(int argc, char *argv[]) ! 581: { ! 582: if (VBE_detect() < 0x200) ! 583: FatalError("This program requires VBE 2.0; Please install UniVBE 5.1."); ! 584: if (argc != 3) ! 585: AvailableModes(); /* Display available modes */ ! 586: ! 587: InitGraphics(atoi(argv[1]),atoi(argv[2])); /* Start graphics */ ! 588: ProfileMode(); /* Profile the video mode */ ! 589: EndGraphics(); /* Restore text mode */ ! 590: ! 591: printf("Profiling results for %dx%d 8 bits per pixel.\n",xres,yres); ! 592: printf("%3.2f clears/s, %2.2f Mb/s\n", clearsPerSec, clearsMbPerSec); ! 593: printf("%3.2f bitBlt/s, %2.2f Mb/s\n", bitBltsPerSec, bitBltsMbPerSec); ! 594: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.