proto-cuda/nvrtc/worker.cpp serves igneum-miner's worker protocol with nothing installed but the NVIDIA driver: nvcuda.dll and the redistributable nvrtc64_120_0.dll are opened with LoadLibrary (cuda_api.h), the pack's kernel.cu and kernel_bound.cu are handed to NVRTC byte for byte up to the host launch wrappers with program.h and memhard.h as named headers, every pack is self-tested against its vectors.h (cache head, last line, FNV-1a 64, dataset head, last word, 64 samples, 96 vector lanes) before it serves a job, prepare runs on a thread for the hourly swap, and a job on seeds without a pair makes the worker find the miner's pack by seeds.txt and build it. packfile.h (C99) reads a pack directory. fetch-redist.sh verifies and stages the NVIDIA 12.8.93 redistributables and the Khronos headers (THIRD-PARTY.md records URLs, hashes and the EULA clause); build-windows.sh cross-compiles with mingw (static, KERNEL32 + Universal CRT only). emu/: the two libraries as host functions, the pack kernels on host threads, the NVRTC source compared with the pack files; test.sh PASS on the Mac (two real packs, swap, self-heal, 17 sampled hashes equal igneum-pow hash-bound). Not run here: the real NVRTC compile and driver load (RTX 5090 PC). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
350 lines
16 KiB
C
350 lines
16 KiB
C
// packfile.h: read a program pack at run time (C99, header only). 4 October 2026.
|
|
//
|
|
// The one-click workers (proto-cuda/nvrtc/worker.cpp and proto-opencl/host.c with --pack) are built once and serve
|
|
// any pack, so the constants that host.cu and host.c take from program.h and vectors.h at compile time are read
|
|
// here from the same files at run time: the dataset and cache sizes, the seed words, the seeds as hex (seeds.txt,
|
|
// written by igneum-miner export-pack and --prepare-packs; program.h carries the same bytes), and the self-test
|
|
// values (cache head, last line and FNV-1a 64; dataset head, last word and 64 samples; the three vector warps).
|
|
// No JSON parser: the headers are scanned for "#define NAME value" and "NAME[..] = { numbers }". Comments are skipped.
|
|
//
|
|
// Included by C (proto-opencl/host.c) and C++ (worker.cpp). Everything is static.
|
|
#ifndef IGNEUM_PACKFILE_H
|
|
#define IGNEUM_PACKFILE_H
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
#define PF_MAX_WARPS 8
|
|
#define PF_MAX_SAMPLES 256
|
|
#define PF_HEX_CAP 520
|
|
|
|
typedef struct {
|
|
// program.h
|
|
uint32_t datasetLog2, cacheLog2Words, cacheSegments, datasetMode, generator;
|
|
uint32_t seedw[8], keyw[8];
|
|
char seedString[600];
|
|
// seeds.txt (or program.h): the seeds as the worker protocol carries them
|
|
char epochHex[65];
|
|
char dayHex[PF_HEX_CAP];
|
|
// vectors.h
|
|
int haveVectors;
|
|
int vecWarps;
|
|
uint32_t vecBase[PF_MAX_WARPS];
|
|
uint64_t vecOut[PF_MAX_WARPS][32];
|
|
uint32_t dsHead[16];
|
|
uint32_t dsLastIndex, dsLast;
|
|
int nSamples;
|
|
uint32_t sampleIdx[PF_MAX_SAMPLES], sampleVal[PF_MAX_SAMPLES];
|
|
uint32_t cacheHead[16], cacheLast[16];
|
|
uint64_t cacheFnv;
|
|
} PfPack;
|
|
|
|
static char* pf_read_file(const char* path, size_t* len) {
|
|
FILE* f = fopen(path, "rb");
|
|
char* buf;
|
|
long n;
|
|
if (!f) return NULL;
|
|
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
|
|
if (n < 0) { fclose(f); return NULL; }
|
|
buf = (char*)malloc((size_t)n + 1);
|
|
if (!buf) { fclose(f); return NULL; }
|
|
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { fclose(f); free(buf); return NULL; }
|
|
buf[n] = 0;
|
|
fclose(f);
|
|
if (len) *len = (size_t)n;
|
|
return buf;
|
|
}
|
|
|
|
static char* pf_read_pack_file(const char* dir, const char* name, size_t* len) {
|
|
char path[2048];
|
|
snprintf(path, sizeof(path), "%s/%s", dir, name);
|
|
return pf_read_file(path, len);
|
|
}
|
|
|
|
// The text after "#define NAME" (NAME followed by a space or tab) at the start of a line, or NULL.
|
|
static const char* pf_find_define(const char* text, const char* name) {
|
|
size_t n = strlen(name);
|
|
const char* p = text;
|
|
while ((p = strstr(p, "#define ")) != NULL) {
|
|
const char* q = p + 8;
|
|
if (p == text || p[-1] == '\n' || p[-1] == '\r') {
|
|
if (strncmp(q, name, n) == 0 && (q[n] == ' ' || q[n] == '\t')) return q + n;
|
|
}
|
|
p = q;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// A number with an optional C suffix (u, U, ull, ULL, l, L). *end is left after the suffix.
|
|
static int pf_number(const char* s, uint64_t* out, const char** end) {
|
|
char* e = NULL;
|
|
unsigned long long v;
|
|
if (!(isdigit((unsigned char)s[0]))) return 0;
|
|
v = strtoull(s, &e, 0);
|
|
if (e == s) return 0;
|
|
while (*e == 'u' || *e == 'U' || *e == 'l' || *e == 'L') ++e;
|
|
*out = (uint64_t)v;
|
|
*end = e;
|
|
return 1;
|
|
}
|
|
|
|
static int pf_define_u32(const char* text, const char* name, uint32_t* out) {
|
|
const char* p = pf_find_define(text, name);
|
|
uint64_t v;
|
|
const char* e;
|
|
if (!p) return 0;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (!pf_number(p, &v, &e)) return 0;
|
|
*out = (uint32_t)v;
|
|
return 1;
|
|
}
|
|
|
|
static int pf_define_str(const char* text, const char* name, char* out, size_t cap) {
|
|
const char* p = pf_find_define(text, name);
|
|
const char* q;
|
|
size_t n;
|
|
if (!p) return 0;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (*p != '"') return 0;
|
|
++p;
|
|
q = strchr(p, '"');
|
|
if (!q) return 0;
|
|
n = (size_t)(q - p);
|
|
if (n + 1 > cap) n = cap - 1;
|
|
memcpy(out, p, n); out[n] = 0;
|
|
return 1;
|
|
}
|
|
|
|
// Numbers inside the braces that follow `start`, comments and commas skipped, nested braces flattened.
|
|
// Returns the count read (at most max). `start` points at or before the opening brace.
|
|
static int pf_brace_numbers(const char* start, uint64_t* out, int max) {
|
|
const char* p = start;
|
|
int depth = 0, n = 0;
|
|
while (*p && *p != '{') { if (*p == ';') return 0; ++p; }
|
|
if (*p != '{') return 0;
|
|
for (;;) {
|
|
uint64_t v;
|
|
const char* e;
|
|
if (!*p) break;
|
|
if (*p == '/' && p[1] == '/') { while (*p && *p != '\n') ++p; continue; }
|
|
if (*p == '/' && p[1] == '*') { const char* c = strstr(p + 2, "*/"); if (!c) break; p = c + 2; continue; }
|
|
if (*p == '{') { ++depth; ++p; continue; }
|
|
if (*p == '}') { --depth; ++p; if (depth == 0) break; continue; }
|
|
if (pf_number(p, &v, &e)) { if (n < max) out[n] = v; ++n; p = e; continue; }
|
|
++p;
|
|
}
|
|
return n < max ? n : max;
|
|
}
|
|
|
|
static int pf_define_words(const char* text, const char* name, uint32_t* out, int max) {
|
|
const char* p = pf_find_define(text, name);
|
|
uint64_t tmp[PF_MAX_SAMPLES];
|
|
int n, i;
|
|
if (!p) return 0;
|
|
if (max > PF_MAX_SAMPLES) max = PF_MAX_SAMPLES;
|
|
n = pf_brace_numbers(p, tmp, max);
|
|
for (i = 0; i < n; ++i) out[i] = (uint32_t)tmp[i];
|
|
return n;
|
|
}
|
|
|
|
// "NAME" as a C symbol: `... NAME[...] = { ... };` or `... NAME = value;`. Returns the numbers found (at most max).
|
|
static int pf_symbol_numbers(const char* text, const char* name, uint64_t* out, int max) {
|
|
size_t n = strlen(name);
|
|
const char* p = text;
|
|
while ((p = strstr(p, name)) != NULL) {
|
|
const char* q = p + n;
|
|
int before = (p == text) || !(isalnum((unsigned char)p[-1]) || p[-1] == '_');
|
|
p = q;
|
|
if (!before) continue;
|
|
if (isalnum((unsigned char)*q) || *q == '_') continue;
|
|
while (*q == '[' ) { const char* c = strchr(q, ']'); if (!c) return 0; q = c + 1; }
|
|
while (*q == ' ' || *q == '\t') ++q;
|
|
if (*q != '=') continue;
|
|
++q;
|
|
while (*q == ' ' || *q == '\t') ++q;
|
|
if (*q == '{') return pf_brace_numbers(q, out, max);
|
|
{ uint64_t v; const char* e; if (pf_number(q, &v, &e)) { if (max > 0) out[0] = v; return max > 0 ? 1 : 0; } }
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int pf_symbol_u32s(const char* text, const char* name, uint32_t* out, int max) {
|
|
uint64_t tmp[PF_MAX_SAMPLES];
|
|
int n, i;
|
|
if (max > PF_MAX_SAMPLES) max = PF_MAX_SAMPLES;
|
|
n = pf_symbol_numbers(text, name, tmp, max);
|
|
for (i = 0; i < n; ++i) out[i] = (uint32_t)tmp[i];
|
|
return n;
|
|
}
|
|
|
|
static int pf_unhex(const char* s, uint8_t* out, size_t cap, size_t* len) {
|
|
size_t n = strlen(s), i;
|
|
if (n % 2 || n / 2 > cap) return 0;
|
|
for (i = 0; i < n; i += 2) {
|
|
unsigned v = 0;
|
|
char two[3];
|
|
two[0] = s[i]; two[1] = s[i + 1]; two[2] = 0;
|
|
if (!isxdigit((unsigned char)two[0]) || !isxdigit((unsigned char)two[1])) return 0;
|
|
if (sscanf(two, "%2x", &v) != 1) return 0;
|
|
out[i / 2] = (uint8_t)v;
|
|
}
|
|
*len = n / 2;
|
|
return 1;
|
|
}
|
|
|
|
// seed_words_from_bytes of igneum-pow/src/seed.rs: FNV-1a 64 with four salts, each finalised.
|
|
static void pf_seed_words_from_bytes(const uint8_t* b, size_t n, uint32_t out[8]) {
|
|
uint64_t salt;
|
|
for (salt = 0; salt < 4; ++salt) {
|
|
uint64_t h = 0xcbf29ce484222325ull ^ (salt * 0x9E3779B97F4A7C15ull);
|
|
size_t i;
|
|
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
|
|
h ^= h >> 33; h *= 0xff51afd7ed558ccdull; h ^= h >> 33;
|
|
out[2 * salt] = (uint32_t)h;
|
|
out[2 * salt + 1] = (uint32_t)(h >> 32);
|
|
}
|
|
}
|
|
|
|
static uint64_t pf_fnv1a64(const void* p, size_t n) {
|
|
const uint8_t* b = (const uint8_t*)p;
|
|
uint64_t h = 0xcbf29ce484222325ull;
|
|
size_t i;
|
|
for (i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
|
|
return h;
|
|
}
|
|
|
|
// One "key value" line of seeds.txt.
|
|
static int pf_seeds_line(const char* text, const char* key, char* out, size_t cap) {
|
|
size_t n = strlen(key);
|
|
const char* p = text;
|
|
while ((p = strstr(p, key)) != NULL) {
|
|
if ((p == text || p[-1] == '\n') && p[n] == ' ') {
|
|
const char* q = p + n + 1;
|
|
size_t len = 0;
|
|
while (q[len] && q[len] != '\r' && q[len] != '\n') ++len;
|
|
if (len + 1 > cap) return 0;
|
|
memcpy(out, q, len); out[len] = 0;
|
|
return 1;
|
|
}
|
|
p += n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int pf_fail(char* err, size_t cap, const char* msg) { if (err && cap) { strncpy(err, msg, cap - 1); err[cap - 1] = 0; } return 0; }
|
|
|
|
// Reads program.h, seeds.txt (optional) and vectors.h (optional) of a pack directory. Returns 1 on success.
|
|
static int pf_load(const char* dir, PfPack* pk, char* err, size_t cap) {
|
|
char* prog;
|
|
char* seeds;
|
|
char* vec;
|
|
char ehex[65] = {0}, dhex[PF_HEX_CAP] = {0};
|
|
uint8_t bytes[256];
|
|
size_t blen;
|
|
memset(pk, 0, sizeof(*pk));
|
|
prog = pf_read_pack_file(dir, "program.h", NULL);
|
|
if (!prog) { char m[480]; snprintf(m, sizeof(m), "cannot read %.400s/program.h", dir); return pf_fail(err, cap, m); }
|
|
if (!pf_define_u32(prog, "IGNEUM_DATASET_LOG2", &pk->datasetLog2)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_DATASET_LOG2"); }
|
|
if (!pf_define_u32(prog, "IGNEUM_DATASET_MODE", &pk->datasetMode)) pk->datasetMode = 0;
|
|
if (pk->datasetMode != 1) { free(prog); return pf_fail(err, cap, "the pack is not memory-hard (IGNEUM_DATASET_MODE 1); the one-click workers serve memory-hard packs only"); }
|
|
if (!pf_define_u32(prog, "IGNEUM_CACHE_LOG2_WORDS", &pk->cacheLog2Words)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_LOG2_WORDS"); }
|
|
if (!pf_define_u32(prog, "IGNEUM_CACHE_SEGMENTS", &pk->cacheSegments)) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_CACHE_SEGMENTS"); }
|
|
if (!pf_define_u32(prog, "IGNEUM_GENERATOR", &pk->generator)) pk->generator = 1;
|
|
if (pf_define_words(prog, "IGNEUM_SEEDW_INIT", pk->seedw, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_SEEDW_INIT with 8 words"); }
|
|
if (pf_define_words(prog, "IGNEUM_KEY_INIT", pk->keyw, 8) != 8) { free(prog); return pf_fail(err, cap, "program.h has no IGNEUM_KEY_INIT with 8 words"); }
|
|
if (!pf_define_str(prog, "IGNEUM_SEED_STRING", pk->seedString, sizeof(pk->seedString))) strncpy(pk->seedString, "(no IGNEUM_SEED_STRING)", sizeof(pk->seedString) - 1);
|
|
pf_define_str(prog, "IGNEUM_SEED_BYTES_HEX", ehex, sizeof(ehex));
|
|
pf_define_str(prog, "IGNEUM_DAY_BYTES_HEX", dhex, sizeof(dhex));
|
|
if (pk->datasetLog2 < 20 || pk->datasetLog2 > 31 || pk->cacheLog2Words < 16 || pk->cacheLog2Words > 30 || pk->cacheSegments == 0) { free(prog); return pf_fail(err, cap, "program.h sizes out of range"); }
|
|
free(prog);
|
|
|
|
// seeds.txt wins when present; program.h's bytes must agree with it
|
|
seeds = pf_read_pack_file(dir, "seeds.txt", NULL);
|
|
if (seeds) {
|
|
char e2[65] = {0}, d2[PF_HEX_CAP] = {0};
|
|
int he = pf_seeds_line(seeds, "epoch_seed_hex", e2, sizeof(e2));
|
|
int hd = pf_seeds_line(seeds, "day_seed_hex", d2, sizeof(d2));
|
|
free(seeds);
|
|
if (!he || !hd) return pf_fail(err, cap, "seeds.txt has no epoch_seed_hex / day_seed_hex line");
|
|
if (ehex[0] && strcmp(ehex, e2) != 0) return pf_fail(err, cap, "seeds.txt epoch_seed_hex differs from program.h IGNEUM_SEED_BYTES_HEX");
|
|
if (dhex[0] && strcmp(dhex, d2) != 0) return pf_fail(err, cap, "seeds.txt day_seed_hex differs from program.h IGNEUM_DAY_BYTES_HEX");
|
|
strcpy(ehex, e2); strcpy(dhex, d2);
|
|
}
|
|
if (!ehex[0] || !dhex[0]) return pf_fail(err, cap, "no seeds: neither seeds.txt nor IGNEUM_SEED_BYTES_HEX / IGNEUM_DAY_BYTES_HEX in program.h (a pack from igneum-pow export --seed <name> has no byte seeds)");
|
|
if (strlen(ehex) != 64) return pf_fail(err, cap, "epoch seed is not 64 hex characters");
|
|
strcpy(pk->epochHex, ehex); strcpy(pk->dayHex, dhex);
|
|
// The seed words derived from the bytes must be the pack's own words: otherwise the pack and the seeds disagree
|
|
{
|
|
uint32_t w[8];
|
|
if (!pf_unhex(ehex, bytes, 32, &blen) || blen != 32) return pf_fail(err, cap, "epoch seed hex is malformed");
|
|
pf_seed_words_from_bytes(bytes, 32, w);
|
|
if (memcmp(w, pk->seedw, 32) != 0) return pf_fail(err, cap, "the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT (wrong seeds.txt for this pack?)");
|
|
if (!pf_unhex(dhex, bytes, sizeof(bytes), &blen)) return pf_fail(err, cap, "day seed hex is malformed");
|
|
pf_seed_words_from_bytes(bytes, blen, w);
|
|
if (memcmp(w, pk->keyw, 32) != 0) return pf_fail(err, cap, "the day seed bytes do not give the pack's IGNEUM_KEY_INIT (wrong seeds.txt for this pack?)");
|
|
}
|
|
|
|
vec = pf_read_pack_file(dir, "vectors.h", NULL);
|
|
if (vec) {
|
|
uint64_t tmp[PF_MAX_WARPS * 32];
|
|
uint32_t warps = 0;
|
|
int n, w, l;
|
|
if (pf_define_u32(vec, "IGNEUM_VEC_WARPS", &warps) && warps >= 1 && warps <= PF_MAX_WARPS) {
|
|
pk->vecWarps = (int)warps;
|
|
n = pf_symbol_u32s(vec, "IGNEUM_VEC_BASE", pk->vecBase, PF_MAX_WARPS);
|
|
if (n != pk->vecWarps) pk->vecWarps = 0;
|
|
n = pf_symbol_numbers(vec, "IGNEUM_VEC_OUT", tmp, PF_MAX_WARPS * 32);
|
|
if (n != pk->vecWarps * 32) pk->vecWarps = 0;
|
|
for (w = 0; w < pk->vecWarps; ++w) for (l = 0; l < 32; ++l) pk->vecOut[w][l] = tmp[w * 32 + l];
|
|
}
|
|
if (pf_symbol_u32s(vec, "IGNEUM_DS_HEAD", pk->dsHead, 16) == 16 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_DS_LAST_INDEX", &pk->dsLastIndex, 1) == 1 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_DS_LAST", &pk->dsLast, 1) == 1 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_CACHE_HEAD", pk->cacheHead, 16) == 16 &&
|
|
pf_symbol_u32s(vec, "IGNEUM_CACHE_LAST", pk->cacheLast, 16) == 16 &&
|
|
pf_symbol_numbers(vec, "IGNEUM_CACHE_FNV64", &pk->cacheFnv, 1) == 1 && pk->vecWarps > 0) {
|
|
uint32_t ns = 0;
|
|
pk->haveVectors = 1;
|
|
if (pf_define_u32(vec, "IGNEUM_DS_SAMPLES", &ns) && ns > 0 && ns <= PF_MAX_SAMPLES) {
|
|
int a = pf_symbol_u32s(vec, "IGNEUM_DS_SAMPLE_INDEX", pk->sampleIdx, (int)ns);
|
|
int b = pf_symbol_u32s(vec, "IGNEUM_DS_SAMPLE_VALUE", pk->sampleVal, (int)ns);
|
|
pk->nSamples = (a == (int)ns && b == (int)ns) ? (int)ns : 0;
|
|
}
|
|
}
|
|
free(vec);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// The self-test verdict from values the host read back from the device. `vec` holds vecWarps x 32 outputs of the
|
|
// bound kernel run with the pack's own seed words as init words (that is igneum_hash of kernel.cu). Writes one line.
|
|
static int pf_selftest(const PfPack* pk, const uint32_t* cacheHead, const uint32_t* cacheLast, uint64_t cacheFnv,
|
|
const uint32_t* dsHead, uint32_t dsLast, const uint32_t* sampleVals, const uint64_t* vec,
|
|
char* out, size_t cap) {
|
|
int okCH = memcmp(cacheHead, pk->cacheHead, 64) == 0, okCL = memcmp(cacheLast, pk->cacheLast, 64) == 0;
|
|
int okFnv = (cacheFnv == pk->cacheFnv);
|
|
int okDH = memcmp(dsHead, pk->dsHead, 64) == 0, okDL = (dsLast == pk->dsLast);
|
|
int badS = 0, badV = 0, i, w, l, firstBadWarp = -1, firstBadLane = -1;
|
|
for (i = 0; i < pk->nSamples; ++i) if (sampleVals[i] != pk->sampleVal[i]) ++badS;
|
|
for (w = 0; w < pk->vecWarps; ++w) for (l = 0; l < 32; ++l) if (vec[w * 32 + l] != pk->vecOut[w][l]) { if (firstBadWarp < 0) { firstBadWarp = w; firstBadLane = l; } ++badV; }
|
|
if (okCH && okCL && okFnv && okDH && okDL && badS == 0 && badV == 0) {
|
|
snprintf(out, cap, "self-test PASS (cache head, last line and FNV-1a 64 %016llx; dataset head, word [%u] and %d samples; %d of %d vector lanes)",
|
|
(unsigned long long)cacheFnv, pk->dsLastIndex, pk->nSamples, pk->vecWarps * 32, pk->vecWarps * 32);
|
|
return 1;
|
|
}
|
|
snprintf(out, cap, "self-test FAIL (cache head %s, cache last %s, cache FNV %016llx vs pack %016llx %s, dataset head %s, dataset last %s, samples %d bad of %d, vector lanes %d bad of %d%s)",
|
|
okCH ? "ok" : "BAD", okCL ? "ok" : "BAD", (unsigned long long)cacheFnv, (unsigned long long)pk->cacheFnv, okFnv ? "ok" : "BAD",
|
|
okDH ? "ok" : "BAD", okDL ? "ok" : "BAD", badS, pk->nSamples, badV, pk->vecWarps * 32,
|
|
firstBadWarp >= 0 ? " (first bad lane in the warp at base nonce" : "");
|
|
if (firstBadWarp >= 0) {
|
|
size_t n = strlen(out);
|
|
snprintf(out + n, cap > n ? cap - n : 0, " %u lane %d: device %016llx expected %016llx)", pk->vecBase[firstBadWarp], firstBadLane,
|
|
(unsigned long long)vec[firstBadWarp * 32 + firstBadLane], (unsigned long long)pk->vecOut[firstBadWarp][firstBadLane]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#endif
|