/* NVClock 0.8 - Linux overclocker for NVIDIA cards * * Copyright(C) 2001-2006 Roderick Colenbrander * * Copyright(C) 2005 Hans-Frieder Vogt * NV40 bios parsing improvements (BIT parsing rewrite + performance table fixes) * * site: http://nvclock.sourceforge.net * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA */ /* TODO: - support for parsing some other init/script tables - support for pre-GeforceFX bioses */ #include "backend.h" #include "nvclock.h" #include #include #include #include #include #include #include #include #define READ_BYTE(rom, offset) (rom[offset]&0xff) #define READ_SHORT(rom, offset) ((rom[offset+1]&0xff) << 8 | (rom[offset]&0xff)) #define READ_INT(rom, offset) ((rom[offset+3]&0xff) << 24 | (rom[offset+2]&0xff) << 16 | (rom[offset+1]&0xff) << 8 | (rom[offset]&0xff)) #define READ_LONG(rom, offset) (READ_INT(rom, offset+4)<<32 | READ_INT(rom, offset)) static unsigned int locate(char *rom, char *str, int offset); struct nvbios *read_bios(char *file); static struct nvbios *parse_bios(char *rom); /* Read a string from a given offset */ static char* nv_read(char *rom, unsigned short offset) { char *res = (char*)strdup(&rom[offset]); short len=0; short i; len = strlen(res); /* Currently we only use this function for reading the signon message. The string ends with a '\n' which we don't want so remove it. */ for(i=0; i>24) & 0xff, (version>>16) & 0xff, (version>>8) & 0xff, version&0xff, '\0'); return (char*)strdup(res); } /* Read the GeforceFX performance table */ static void nv30_read_performance(struct nvbios *bios, char *rom, int offset) { int j = 0; unsigned char start = 0; unsigned char size = 0; int tmp = 0; /* read how far away the start is */ start = rom[offset]; size = rom[offset + 3]; tmp = offset + start + 1; bios->perf_entries=3; for(j=0; j < 3; j++) { bios->perf_lst[j].nvclk = (READ_INT(rom, tmp))/100; /* The list can contain multiple distinct memory clocks. / Later on the ramcfg register can tell which of the ones is the right one. / But for now assume the first one is correct. It doesn't matter much if the / clocks are a little lower/higher as we mainly use this to detect 3d clocks / / Further the clock stored here is the 'real' memory frequency, the effective one / is twice as high. It doesn't seem to be the case for all bioses though. In some effective / and real speed entries existed but this might be patched dumps. */ bios->perf_lst[j].memclk = (READ_INT(rom, tmp+4))/50; /* Move behind the timing stuff to the fanspeed and voltage */ bios->perf_lst[j].fanspeed = (float)(unsigned char)rom[tmp + 54]; bios->perf_lst[j].voltage = (float)(unsigned char)rom[tmp + 55]/100; /* In case the voltage is 0, assume the voltage is similar to the previous voltage */ if(bios->perf_lst[j].voltage==0 && j>0) bios->perf_lst[j].voltage = bios->perf_lst[j-1].voltage; tmp = offset + start + (j+1)*size + 1; } } /* Convert the bios version which is stored in a numeric way to a string. / On NV40 bioses it is stored in 5 numbers instead of 4 which was the / case on old cards. The bios version on old cards could be bigger than / 4 numbers too but that version was only stored in a string which was / hard to locate. On NV40 cards the version is stored in a string too, / for which the offset can be found at +3 in the 'S' table. */ static char *nv40_bios_version_to_str(char *rom, short offset) { char res[15]; int version = READ_INT(rom, offset); char extra = rom[offset+4]; sprintf(res, "%02X.%02x.%02x.%02x.%02x%c", (version>>24) & 0xff, (version>>16) & 0xff, (version>>8) & 0xff, version&0xff, extra, '\0'); return (char*)strdup(res); } /* Init script tables contain dozens of entries containing commands to initialize / the card. There are lots of different commands each having a different 'id' useally / most entries also have a different size. The task of this function is to move to the / next entry in the table. */ static int nv40_init_script_table_get_next_entry(char *rom, int offset) { unsigned char id = rom[offset]; switch(id) { case '2': /* 0x32 */ offset += 43; break; case '3': /* 0x33 */ offset += 2; break; case '6': /* 0x36 */ offset += 1; break; case '7': /* 0x37 */ offset += 11; break; case '8': /* 0x38 */ offset += 1; break; case '9': /* 0x39 */ offset += 2; break; case 'J': /* 0x4A */ offset += 43; break; case 'K': /* 0x4B */ #if DEBUG /* +1 = PLL register, +5 = value */ printf("'%c'\t%08x %08x\n", id, READ_INT(rom, offset+1), READ_INT(rom, offset+5)); #endif offset += 9; break; case 'Q': /* 0x51 */ offset += 5 + rom[offset+4]; break; case 'R': /* 0x52 */ offset += 4; break; case 'S': /* 0x53 */ offset += 3; break; case 'T': /* 0x54 */ offset += 2 + rom[offset+1] * 2; break; case 'V': /* 0x56 */ offset += 3; break; case 'X': /* 0x58 */ offset += 6 + rom[offset+5] * 4; break; case '[': /* 0x5b */ offset += 3; break; case '_': /* 0x5F */ offset += 22; break; case 'b': /* 0x62 */ offset += 5; break; case 'c': /* 0x63 */ offset +=1; break; case 'e': /* 0x65 */ offset += 13; break; case 'k': /* 0x6b */ offset += 2; break; case 'n': /* 0x6e */ #if DEBUG /* +1 = register, +5 = AND-mask, +9 = value */ printf("'%c'\t%08x %08x %08x\n", id, READ_INT(rom, offset+1), READ_INT(rom, offset+5), READ_INT(rom, offset+9)); #endif offset += 13; break; case 'o': /* 0x6f */ offset += 2; break; case 'q': /* 0x71: quit */ offset += 1; break; case 'r': /* 0x72 */ offset += 1; break; case 't': /* 0x74 */ offset += 3; break; case 'u': /* 0x75 */ offset += 2; break; case 'x': /* 0x78 */ offset += 6; break; case 'y': /* 0x79 */ #if DEBUG /* +1 = register, +5 = clock */ printf("'%c'\t%08x %08x (%dMHz)\n", id, READ_INT(rom, offset+1), READ_SHORT(rom, offset+5), READ_SHORT(rom, offset+5)/100); #endif offset += 7; break; case 'z': /* 0x7a */ #if DEBUG /* +1 = register, +5 = value */ printf("'%c'\t%08x %08x\n", id, READ_INT(rom, offset+1), READ_INT(rom, offset+5)); #endif offset += 9; break; case 0x8f: /* 0x8f */ /* +5 = size of entry in bytes, +6 = num entries */ offset += READ_BYTE(rom, offset+6) * 32 + 7; break; case 0x90: /* 0x90 */ offset += 9; break; case 0x91: /* 0x91 */ #if DEBUG /* +1 = pll register, +5 = ?, +9 = ?, +13 = ? */ printf("'%c'\t%08x %08x\n", id, READ_INT(rom, offset+1), READ_INT(rom, offset+5)); #endif offset += 18; break; } return offset; } static void nv40_read_init_script_table(struct nvbios *bios, char *rom, int init_offset, int len) { int i,offset; int done=0; unsigned char id; /* Table 1 */ offset = READ_SHORT(rom, init_offset); /* For pipeline modding purposes we cache 0x1540 */ id = rom[offset]; while(id != 'q' && !done) { offset = nv40_init_script_table_get_next_entry(rom, offset); id = rom[offset]; if(id == 'z' && READ_INT(rom, offset+1) == 0x1540) { bios->pipe_cfg = READ_INT(rom, offset+5); done=1; } } #if DEBUG /* Read all init tables and print some debug info */ /* Table 1 */ offset = READ_SHORT(rom, init_offset); for(i=0; i<=len; i+=2) { /* Not all tables have to exist */ if(!offset) { init_offset += 2; offset = READ_SHORT(rom, init_offset); continue; } printf("Init script table %d\n", i/2+1); id = rom[offset]; while(id != 'q') { if(!(id == 'K' || id == 'n' || id == 'x' || id == 'y' || id == 'z')) printf("'%c' (%x)\n", id, id); offset = nv40_init_script_table_get_next_entry(rom, offset); id = rom[offset]; } /* Pointer to next init table */ init_offset += 2; /* Get location of next table */ offset = READ_SHORT(rom, init_offset); } #endif } /* Read the Geforce6 performance table */ static void nv40_read_performance(struct nvbios *bios, char *rom, int offset) { short i, num_entries; unsigned char size; unsigned char start; /* find start offset of entries, at the moment it seems to be always 0x0b */ start = rom[offset+1]; /* It seems that at +2 information is stored about the number of active / entries in the performance table. The only thing I don't know yet is / where to find which entry should be chosen. For all geforce6800(LE/GT/NU/Ultra) / bioses I tried, the first entry seemed correct but for a 6200 bios it wasn't. / (Note that 6200 bioses also work a little differently) Currently this assumption / is 'correct' until I have more information. */ if(rom[offset+2]) num_entries = rom[offset+2]; else num_entries = 1; /* +5 contains the number of entries, +4 the size of one in bytes and +3 is some 'offset' */ size = rom[offset+3] + rom[offset+4] * rom[offset+5]; /* now read entries / entries start with 0x20 for entry 0, 0x21 for entry 1, ... */ offset += start; for(i=0; iperf_lst[i].fanspeed = (unsigned char)rom[offset+4]; bios->perf_lst[i].voltage = (float)(unsigned char)rom[offset+5]/100; /* In case the voltage is 0, assume the voltage is similar to the previous voltage */ if(bios->perf_lst[i].voltage==0 && i>0) bios->perf_lst[i].voltage = bios->perf_lst[i-1].voltage; /* HACK: My collection of bioses contains a (valid) 6600 bios with two 'bogus' entries at 0x21 (100MHz) and 0x22 (200MHz) / these entries aren't the default ones for sure, so skip them until we have a better entry selection algorithm. */ if(READ_SHORT(rom, offset+6) > 200) { bios->perf_lst[i].nvclk = READ_SHORT(rom, offset+6); /* Support delta clock reading on some NV4X boards. The entries seem to be present on most Geforce7 boards but are as far as I know only used on 7800/7900 boards. / On other boards the delta clocks are set to 0. Offset +8 contains the actual delta clock and offset +7 contains a divider for it. If the divider is 0 we don't read the delta clock. */ if((get_gpu_arch(bios->device_id) & (NV47 | NV49)) && rom[offset+7]) bios->perf_lst[i].delta = rom[offset+8]/rom[offset+7]; bios->perf_lst[i].memclk = READ_SHORT(rom, offset+11)*2; bios->perf_entries = i+1; i++; } offset += size; } } /* The internal gpu sensor most likely consists of a diode and a resistor. / The voltage across this resistor is meassured using a ADC. Since the / voltage-current relationship of a diode isn't linear the value needs some correction. / The temperature can be calculated by scaling the output value of the ADC and adding an offset / to it. / / This function reads the temperature table and reads the offset/scaling constants for the / temperature calculation formula. Before I didn't know where and how these values were stored and / used some hardcoded (wrong) values. I expected the values to be tored near the place where / the temperature sensor enable/disable bit was but I didn't have the time to figure it all out. / The code below is very similar to the code from the Rivatuner gpu diode by Alexey Nicolaychuk with a few adjustments. / Rivatuner's code didn't contain constants for the latest Geforce7 (NV46/NV49/NV4B) cards so I had to add those myself. */ static void nv40_read_temperature(struct nvbios *bios, char *rom, int offset) { short i; unsigned char num_entries = rom[offset+3]; unsigned char size = rom[offset+2]; unsigned char start = rom[offset+1]; switch(get_gpu_arch(bios->device_id)) { case NV43: bios->sensor_cfg.diode_offset_mult = 32060; bios->sensor_cfg.diode_offset_div = 1000; bios->sensor_cfg.slope_mult = 792; bios->sensor_cfg.slope_div = 1000; break; case NV44: case NV47: bios->sensor_cfg.diode_offset_mult = 27839; bios->sensor_cfg.diode_offset_div = 1000; bios->sensor_cfg.slope_mult = 780; bios->sensor_cfg.slope_div = 1000; break; case NV46: /* are these really the default ones? they come from a 7300GS bios */ bios->sensor_cfg.diode_offset_mult = -24775; bios->sensor_cfg.diode_offset_div = 100; bios->sensor_cfg.slope_mult = 467; bios->sensor_cfg.slope_div = 10000; break; case NV49: /* are these really the default ones? they come from a 7900GT/GTX bioses */ bios->sensor_cfg.diode_offset_mult = -25051; bios->sensor_cfg.diode_offset_div = 100; bios->sensor_cfg.slope_mult = 458; bios->sensor_cfg.slope_div = 10000; break; case NV4B: /* are these really the default ones? they come from a 7600GT bios */ bios->sensor_cfg.diode_offset_mult = -24088; bios->sensor_cfg.diode_offset_div = 100; bios->sensor_cfg.slope_mult = 442; bios->sensor_cfg.slope_div = 10000; break; } for(i=0; i>9) & 0x7f, value & 0x3ff); #endif if((value & 0x8f) == 0) bios->sensor_cfg.temp_correction = (value>>9) & 0x7f; break; /* An id of 4 seems to correspond to a temperature threshold but 5, 6 and 8 have similar values, what are they? */ case 0x4: case 0x5: case 0x6: case 0x8: /* printf("0x%x: 0x%x %d\n", id, value & 0xf, (value>>4) & 0x1ff); */ break; case 0x10: bios->sensor_cfg.diode_offset_mult = value; break; case 0x11: bios->sensor_cfg.diode_offset_div = value; break; case 0x12: bios->sensor_cfg.slope_mult = value; break; case 0x13: bios->sensor_cfg.slope_div = value; break; #if DEBUG default: printf("0x%x: %x\n", id, value); #endif } } #if DEBUG printf("correction: %d\n", bios->sensor_cfg.temp_correction); printf("offset: %.3f\n", (float)bios->sensor_cfg.diode_offset_mult / (float)bios->sensor_cfg.diode_offset_div); printf("slope: %.3f\n", (float)bios->sensor_cfg.slope_mult / (float)bios->sensor_cfg.slope_div); #endif } /* Read the voltage table for nv30/nv40 cards */ static void read_voltage(struct nvbios *bios, char *rom, int offset) { unsigned char entry_size=0; int i; entry_size = rom[offset+1]; bios->volt_entries = rom[offset+2]; bios->volt_mask = rom[offset+4]; for(i=0; ivolt_entries; i++) { bios->volt_lst[i].voltage = (float)(unsigned char)rom[offset + 5 + entry_size * i] / 100; bios->volt_lst[i].VID = rom[offset + 6 + entry_size * i]; } } static void nv5_parse(struct nvbios *bios, char *rom, unsigned short nv_offset) { /* Go to the position containing the offset to the card name, it is 30 away from NV. */ int offset = READ_SHORT(rom, nv_offset + 30); bios->signon_msg = nv_read(rom, offset); } static void nv30_parse(struct nvbios *bios, char *rom, unsigned short nv_offset) { unsigned short init_offset = 0; unsigned short perf_offset=0; unsigned short volt_offset=0; int offset = READ_SHORT(rom, nv_offset + 30); bios->signon_msg = nv_read(rom, offset); init_offset = READ_SHORT(rom, nv_offset + 0x4d); volt_offset = READ_SHORT(rom, nv_offset + 0x98); read_voltage(bios, rom, volt_offset); perf_offset = READ_SHORT(rom, nv_offset + 0x94); nv30_read_performance(bios, rom, perf_offset); } static void nv40_parse(struct nvbios *bios, char *rom, unsigned int bit_offset) { unsigned short init_offset=0; unsigned short perf_offset=0; unsigned short signon_offset=0; unsigned short temp_offset=0; unsigned short volt_offset=0; unsigned short offset=0; struct bit_entry { unsigned char id[2]; /* first byte is ID, second byte sub-ID? */ unsigned short len; /* size of data pointed to by offset */ unsigned short offset; /* offset of data */ } *entry; /* In older nvidia bioses there was some start position and at fixed positions from there offsets to various tables were stored. / For Geforce6 bioses this is all different. There is still some start position (now called BIT) but offsets to tables aren't at fixed / positions from the start. There's now some weird pattern which starts a few places from the start of the BIT section. / This pattern seems to consist of a subset of the alphabet (all in uppercase). After each such token there is the length of the data / referred to by the entry and an offset. The first entry "0x00 0x01" is probably somewhat different since the length/offset info / seems to be a bit strange. The list ends with the entry "0x00 0x00" */ /* skip 'B' 'I' 'T' '\0' */ offset = bit_offset + 4; /* read the entries */ while (1) { entry = (struct bit_entry *)&rom[offset]; if ((entry->id[0] == 0) && (entry->id[1] == 0)) break; switch (entry->id[0]) { case 'B': /* BIOS related data */ bios->version = nv40_bios_version_to_str(rom, entry->offset); break; case 'I': /* Init table */ init_offset = READ_SHORT(rom, entry->offset); nv40_read_init_script_table(bios, rom, init_offset, entry->len); break; case 'P': /* Performance related data */ perf_offset = READ_SHORT(rom, entry->offset); nv40_read_performance(bios, rom, perf_offset); temp_offset = READ_SHORT(rom, entry->offset + 0xc); nv40_read_temperature(bios, rom, temp_offset); /* 0x10 behind perf_offset the voltage table offset is stored */ volt_offset = READ_SHORT(rom, entry->offset + 0x10); read_voltage(bios, rom, volt_offset); break; case 'S': /* table with string references of signon-message, BIOS version, BIOS copyright, OEM string, VESA vendor, VESA Product Name, and VESA Product Rev. table consists of offset, max-string-length pairs for all strings */ signon_offset = READ_SHORT(rom, entry->offset); bios->signon_msg = nv_read(rom, signon_offset); break; } offset += sizeof(struct bit_entry); } } static unsigned int locate(char *rom, char *str, int offset) { int size = strlen(str); int i; char* data; /* We shouldn't assume this is allways 64kB */ for(i=offset; i<0xffff; i++) { data = (char*)&rom[i]; if(strncmp(data, str, size) == 0) { return i; } } return 0; } #if DEBUG int main(int argc, char **argv) { read_bios("bios.rom"); return 0; } #else void dump_bios(char *filename) { int i; FILE *fp = NULL; /* enable bios parsing; on some boards the display might turn off */ nv_card->PMC[0x1850/4] = 0x0; /* try to dump the bios */ fp = fopen(filename, "w+"); for(i=0; i < 0xffff; i++) { unsigned char data; /* On some 6600GT/6800LE boards bios there are issues with the rom. / Normaly when you want to read data from lets say address X, you receive / the data when it is ready. For some roms the outputs aren't "stable" yet when / we want to read out the data. A workaround from Unwinder is to try to access the location / several times in the hope that the outputs will become stable. In the case of instablity / each fourth byte was wrong (needs to be shifted 4 to the left) and furhter there was some garbage / / A delay of 4 extra reads helps for most 6600GT cards but for 6800Go cards atleast 5 are needed. */ data = nv_card->PROM[i]; data = nv_card->PROM[i]; data = nv_card->PROM[i]; data = nv_card->PROM[i]; data = nv_card->PROM[i]; fprintf(fp, "%c", data); } fclose(fp); /* disable the rom; if we don't do it the screens stays black on some cards */ nv_card->PMC[0x1850/4] = 0x1; } #endif /* This function tries to read a copy of the bios from harddrive. If that doesn't exist it will dump the bios and then read it. You might wonder why we don't read the bios from card. The reason behind that is that some bioses are slow to read (can take seconds) and second on some cards (atleast on my gf2mx) the screen becomes black if I enable reading of the rom. */ struct nvbios *read_bios(char *file) { int fd = 0; char *rom = NULL; struct nvbios *res; if((fd = open(file, O_RDONLY)) == -1) { /* we need to redump the bios */ return 0; } rom = mmap(0, 0xffff, PROT_READ, MAP_SHARED, fd, 0); /* Do the actual bios parsing */ res = parse_bios(rom); /* Close the bios */ close(fd); return res; } struct nvbios *parse_bios(char *rom) { unsigned short bit_offset = 0; unsigned short nv_offset = 0; unsigned short pcir_offset = 0; unsigned short device_id = 0; struct nvbios *bios; int i=0; /* All bioses start with this '0x55 0xAA' signature */ if((rom[0] != 0x55) || (rom[1] != (char)0xAA)) return NULL; /* Fail when the PCIR header can't be found; it is present on all PCI bioses */ if(!(pcir_offset = locate(rom, "PCIR", 0))) return NULL; /* Fail if the bios is not from an Nvidia card */ if(READ_SHORT(rom, pcir_offset + 4) != 0x10de) return NULL; device_id = READ_SHORT(rom, pcir_offset + 6); if(get_gpu_arch(device_id) & NV4X) { /* For NV40 card the BIT structure is used instead of the BMP structure (last one doesn't exist anymore on 6600/6800le cards). */ if(!(bit_offset = locate(rom, "BIT", 0))) return NULL; bios = calloc(1, sizeof(struct nvbios)); bios->device_id = device_id; nv40_parse(bios, rom, bit_offset); } /* We are dealing with a card that only contains the BMP structure */ else { int version; /* The main offset starts with "0xff 0x7f NV" */ if(!(nv_offset = locate(rom, "\xff\x7fNV", 0))) return NULL; /* We don't support old bioses. Mainly some old tnt1 models */ if(rom[nv_offset + 5] < 5) return NULL; bios = calloc(1, sizeof(struct nvbios)); bios->device_id = device_id; bios->major = (char)rom[nv_offset + 5]; bios->minor = (char)rom[nv_offset + 6]; /* Go to the bios version */ /* Not perfect for bioses containing 5 numbers */ version = READ_INT(rom, nv_offset + 10); bios->version = bios_version_to_str(version); /* Use nv30_parse for all NV3X cards; for overclocking purposes the 5200 is considered / a NV25 card but in this case it really is a NV3X board. */ if((get_gpu_arch(device_id) & NV3X) || ((device_id & 0xff0) == 0x320)) nv30_parse(bios, rom, nv_offset); else nv5_parse(bios, rom, nv_offset); } #if DEBUG printf("signon_msg: %s\n", bios->signon_msg); printf("bios: %s\n", bios->version); printf("BMP version: %x.%x\n", bios->major, bios->minor); for(i=0; i< bios->volt_entries; i++) printf("volt: %.2fV\n", bios->volt_lst[i].voltage); for(i=0; i< bios->perf_entries; i++) { printf("gpu freq: %dMHz @ %.2fV\n", bios->perf_lst[i].nvclk, bios->perf_lst[i].voltage); printf("mem freq: %dMHz\n", bios->perf_lst[i].memclk); } if(bios) { int i; printf("-- VideoBios information --\n"); printf("Version: %s\n", bios->version); printf("Signon message: %s\n", bios->signon_msg); for(i=0; i< bios->perf_entries; i++) { if(bios->volt_entries) { if(bios->perf_lst[i].delta) /* For now assume the first memory entry is the right one; should be fixed as some bioses contain various different entries */ printf("Performance level %d: gpu %d(+%d)MHz/memory %dMHz/ %.2fV / %d%%\n", i, bios->perf_lst[i].nvclk, bios->perf_lst[i].delta, bios->perf_lst[i].memclk, bios->perf_lst[i].voltage, bios->perf_lst[i].fanspeed); else printf("Performance level %d: gpu %dMHz/memory %dMHz/ %.2fV / %d%%\n", i, bios->perf_lst[i].nvclk, bios->perf_lst[i].memclk, bios->perf_lst[i].voltage, bios->perf_lst[i].fanspeed); } else printf("Performance level %d: %dMHz / %dMHz / %d%%\n", i, bios->perf_lst[i].nvclk, bios->perf_lst[i].memclk, bios->perf_lst[i].fanspeed); } if(bios->volt_entries) printf("VID mask: %x\n", bios->volt_mask); for(i=0; i< bios->volt_entries; i++) { /* For now assume the first memory entry is the right one; should be fixed as some bioses contain various different entries */ /* Note that voltage entries in general don't correspond to performance levels!! */ printf("Voltage level %d: %.2fV, VID: %x\n", i, bios->volt_lst[i].voltage, bios->volt_lst[i].VID); } printf("\n"); } #endif return bios; }