source: CLRX/CLRadeonExtender/trunk/amdbin/ROCmBinaries.cpp @ 3420

Last change on this file since 3420 was 3420, checked in by matszpk, 18 months ago

CLRadeonExtender: Commenting AmdCL2BinGen and ROCmBinaries.

File size: 18.9 KB
Line 
1/*
2 *  CLRadeonExtender - Unofficial OpenCL Radeon Extensions Library
3 *  Copyright (C) 2014-2017 Mateusz Szpakowski
4 *
5 *  This library is free software; you can redistribute it and/or
6 *  modify it under the terms of the GNU Lesser General Public
7 *  License as published by the Free Software Foundation; either
8 *  version 2.1 of the License, or (at your option) any later version.
9 *
10 *  This library is distributed in the hope that it will be useful,
11 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 *  Lesser General Public License for more details.
14 *
15 *  You should have received a copy of the GNU Lesser General Public
16 *  License along with this library; if not, write to the Free Software
17 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19
20#include <CLRX/Config.h>
21#include <cassert>
22#include <cstdint>
23#include <algorithm>
24#include <utility>
25#include <CLRX/amdbin/ElfBinaries.h>
26#include <CLRX/utils/Utilities.h>
27#include <CLRX/utils/MemAccess.h>
28#include <CLRX/utils/InputOutput.h>
29#include <CLRX/utils/Containers.h>
30#include <CLRX/amdbin/ROCmBinaries.h>
31
32using namespace CLRX;
33
34/* TODO: add support for various kernel code offset (now only 256 is supported) */
35
36ROCmBinary::ROCmBinary(size_t binaryCodeSize, cxbyte* binaryCode, Flags creationFlags)
37        : ElfBinary64(binaryCodeSize, binaryCode, creationFlags),
38          regionsNum(0), codeSize(0), code(nullptr)
39{
40    cxuint textIndex = SHN_UNDEF;
41    try
42    { textIndex = getSectionIndex(".text"); }
43    catch(const Exception& ex)
44    { } // ignore failed
45    uint64_t codeOffset = 0;
46    // find '.text' section
47    if (textIndex!=SHN_UNDEF)
48    {
49        code = getSectionContent(textIndex);
50        const Elf64_Shdr& textShdr = getSectionHeader(textIndex);
51        codeSize = ULEV(textShdr.sh_size);
52        codeOffset = ULEV(textShdr.sh_offset);
53    }
54   
55    // counts regions (symbol or kernel)
56    regionsNum = 0;
57    const size_t symbolsNum = getSymbolsNum();
58    for (size_t i = 0; i < symbolsNum; i++)
59    {
60        // count regions number
61        const Elf64_Sym& sym = getSymbol(i);
62        const cxbyte symType = ELF64_ST_TYPE(sym.st_info);
63        const cxbyte bind = ELF64_ST_BIND(sym.st_info);
64        if (ULEV(sym.st_shndx)==textIndex &&
65            (symType==STT_GNU_IFUNC || symType==STT_FUNC ||
66                (bind==STB_GLOBAL && symType==STT_OBJECT)))
67            regionsNum++;
68    }
69    if (code==nullptr && regionsNum!=0)
70        throw Exception("No code if regions number is not zero");
71    regions.reset(new ROCmRegion[regionsNum]);
72    size_t j = 0;
73    typedef std::pair<uint64_t, size_t> RegionOffsetEntry;
74    std::unique_ptr<RegionOffsetEntry[]> symOffsets(new RegionOffsetEntry[regionsNum]);
75   
76    // get regions info
77    for (size_t i = 0; i < symbolsNum; i++)
78    {
79        const Elf64_Sym& sym = getSymbol(i);
80        if (ULEV(sym.st_shndx)!=textIndex)
81            continue;   // if not in '.text' section
82        const size_t value = ULEV(sym.st_value);
83        if (value < codeOffset)
84            throw Exception("Region offset is too small!");
85        const size_t size = ULEV(sym.st_size);
86       
87        const cxbyte symType = ELF64_ST_TYPE(sym.st_info);
88        const cxbyte bind = ELF64_ST_BIND(sym.st_info);
89        if (symType==STT_GNU_IFUNC || symType==STT_FUNC ||
90                (bind==STB_GLOBAL && symType==STT_OBJECT))
91        {
92            ROCmRegionType type = ROCmRegionType::DATA;
93            // if kernel
94            if (symType==STT_GNU_IFUNC) 
95                type = ROCmRegionType::KERNEL;
96            // if function kernel
97            else if (symType==STT_FUNC)
98                type = ROCmRegionType::FKERNEL;
99            symOffsets[j] = std::make_pair(value, j);
100            if (type!=ROCmRegionType::DATA && value+0x100 > codeOffset+codeSize)
101                throw Exception("Kernel or code offset is too big!");
102            regions[j++] = { getSymbolName(i), size, value, type };
103        }
104    }
105    // sort regions by offset
106    std::sort(symOffsets.get(), symOffsets.get()+regionsNum,
107            [](const RegionOffsetEntry& a, const RegionOffsetEntry& b)
108            { return a.first < b.first; });
109    // checking distance between regions
110    for (size_t i = 1; i <= regionsNum; i++)
111    {
112        size_t end = (i<regionsNum) ? symOffsets[i].first : codeOffset+codeSize;
113        ROCmRegion& region = regions[symOffsets[i-1].second];
114        if (region.type==ROCmRegionType::KERNEL && symOffsets[i-1].first+0x100 > end)
115            throw Exception("Kernel size is too small!");
116       
117        const size_t regSize = end - symOffsets[i-1].first;
118        if (region.size==0)
119            region.size = regSize;
120        else
121            region.size = std::min(regSize, region.size);
122    }
123   
124    if (hasRegionMap())
125    {
126        // create region map
127        regionsMap.resize(regionsNum);
128        for (size_t i = 0; i < regionsNum; i++)
129            regionsMap[i] = std::make_pair(regions[i].regionName, i);
130        // sort region map
131        mapSort(regionsMap.begin(), regionsMap.end());
132    }
133}
134
135struct AMDGPUArchValuesEntry
136{
137    uint32_t major;
138    uint32_t minor;
139    uint32_t stepping;
140    GPUDeviceType deviceType;
141};
142
143// list of AMDGPU arch entries for GPU devices
144static const AMDGPUArchValuesEntry amdGpuArchValuesTbl[] =
145{
146    { 0, 0, 0, GPUDeviceType::CAPE_VERDE },
147    { 7, 0, 0, GPUDeviceType::BONAIRE },
148    { 7, 0, 1, GPUDeviceType::HAWAII },
149    { 8, 0, 0, GPUDeviceType::ICELAND },
150    { 8, 0, 1, GPUDeviceType::CARRIZO },
151    { 8, 0, 2, GPUDeviceType::ICELAND },
152    { 8, 0, 3, GPUDeviceType::FIJI },
153    { 8, 0, 4, GPUDeviceType::FIJI },
154    { 8, 1, 0, GPUDeviceType::STONEY },
155    { 9, 0, 0, GPUDeviceType::GFX900 },
156    { 9, 0, 1, GPUDeviceType::GFX901 }
157};
158
159static const size_t amdGpuArchValuesNum = sizeof(amdGpuArchValuesTbl) /
160                sizeof(AMDGPUArchValuesEntry);
161
162
163/// determint GPU device from ROCm notes
164GPUDeviceType ROCmBinary::determineGPUDeviceType(uint32_t& outArchMinor,
165                     uint32_t& outArchStepping) const
166{
167    uint32_t archMajor = 0;
168    uint32_t archMinor = 0;
169    uint32_t archStepping = 0;
170   
171    {
172        const cxbyte* noteContent = (const cxbyte*)getNotes();
173        if (noteContent==nullptr)
174            throw Exception("Missing notes in inner binary!");
175        size_t notesSize = getNotesSize();
176        // find note about AMDGPU
177        for (size_t offset = 0; offset < notesSize; )
178        {
179            const Elf64_Nhdr* nhdr = (const Elf64_Nhdr*)(noteContent + offset);
180            size_t namesz = ULEV(nhdr->n_namesz);
181            size_t descsz = ULEV(nhdr->n_descsz);
182            if (usumGt(offset, namesz+descsz, notesSize))
183                throw Exception("Note offset+size out of range");
184            if (ULEV(nhdr->n_type) == 0x3 && namesz==4 && descsz>=0x1a &&
185                ::strcmp((const char*)noteContent+offset+sizeof(Elf64_Nhdr), "AMD")==0)
186            {    // AMDGPU type
187                const uint32_t* content = (const uint32_t*)
188                        (noteContent+offset+sizeof(Elf64_Nhdr) + 4);
189                archMajor = ULEV(content[1]);
190                archMinor = ULEV(content[2]);
191                archStepping = ULEV(content[3]);
192            }
193            size_t align = (((namesz+descsz)&3)!=0) ? 4-((namesz+descsz)&3) : 0;
194            offset += sizeof(Elf64_Nhdr) + namesz + descsz + align;
195        }
196    }
197    // determine device type
198    GPUDeviceType deviceType = GPUDeviceType::CAPE_VERDE;
199    // choose lowest GPU device by archMajor by default
200    if (archMajor==0)
201        deviceType = GPUDeviceType::CAPE_VERDE;
202    else if (archMajor==7)
203        deviceType = GPUDeviceType::BONAIRE;
204    else if (archMajor==8)
205        deviceType = GPUDeviceType::ICELAND;
206    else if (archMajor==9)
207        deviceType = GPUDeviceType::GFX900;
208   
209    // recognize device type by arch major, minor and stepping
210    for (cxuint i = 0; i < amdGpuArchValuesNum; i++)
211        if (amdGpuArchValuesTbl[i].major==archMajor &&
212            amdGpuArchValuesTbl[i].minor==archMinor &&
213            amdGpuArchValuesTbl[i].stepping==archStepping)
214        {
215            deviceType = amdGpuArchValuesTbl[i].deviceType;
216            break;
217        }
218    outArchMinor = archMinor;
219    outArchStepping = archStepping;
220    return deviceType;
221}
222
223const ROCmRegion& ROCmBinary::getRegion(const char* name) const
224{
225    RegionMap::const_iterator it = binaryMapFind(regionsMap.begin(),
226                             regionsMap.end(), name);
227    if (it == regionsMap.end())
228        throw Exception("Can't find region name");
229    return regions[it->second];
230}
231
232// if ROCm binary
233bool CLRX::isROCmBinary(size_t binarySize, const cxbyte* binary)
234{
235    if (!isElfBinary(binarySize, binary))
236        return false;
237    if (binary[EI_CLASS] != ELFCLASS64)
238        return false;
239    const Elf64_Ehdr* ehdr = reinterpret_cast<const Elf64_Ehdr*>(binary);
240    if (ULEV(ehdr->e_machine) != 0xe0 || ULEV(ehdr->e_flags)!=0)
241        return false;
242    return true;
243}
244
245
246void ROCmInput::addEmptyKernel(const char* kernelName)
247{
248    symbols.push_back({ kernelName, 0, 0, ROCmRegionType::KERNEL });
249}
250/*
251 * ROCm Binary Generator
252 */
253
254ROCmBinGenerator::ROCmBinGenerator() : manageable(false), input(nullptr)
255{ }
256
257ROCmBinGenerator::ROCmBinGenerator(const ROCmInput* rocmInput)
258        : manageable(false), input(rocmInput)
259{ }
260
261ROCmBinGenerator::ROCmBinGenerator(GPUDeviceType deviceType,
262        uint32_t archMinor, uint32_t archStepping, size_t codeSize, const cxbyte* code,
263        const std::vector<ROCmSymbolInput>& symbols)
264{
265    input = new ROCmInput{ deviceType, archMinor, archStepping, symbols, codeSize, code };
266}
267
268ROCmBinGenerator::ROCmBinGenerator(GPUDeviceType deviceType,
269        uint32_t archMinor, uint32_t archStepping, size_t codeSize, const cxbyte* code,
270        std::vector<ROCmSymbolInput>&& symbols)
271{
272    input = new ROCmInput{ deviceType, archMinor, archStepping, std::move(symbols),
273                codeSize, code };
274}
275
276ROCmBinGenerator::~ROCmBinGenerator()
277{
278    if (manageable)
279        delete input;
280}
281
282void ROCmBinGenerator::setInput(const ROCmInput* input)
283{
284    if (manageable)
285        delete input;
286    manageable = false;
287    this->input = input;
288}
289
290// ELF notes contents
291static const cxbyte noteDescType1[8] =
292{ 2, 0, 0, 0, 1, 0, 0, 0 };
293
294static const cxbyte noteDescType3[27] =
295{ 4, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
296  'A', 'M', 'D', 0, 'A', 'M', 'D', 'G', 'P', 'U', 0 };
297
298// section index for symbol binding
299static const uint16_t mainBuiltinSectionTable[] =
300{
301    10, // ELFSECTID_SHSTRTAB
302    11, // ELFSECTID_STRTAB
303    9, // ELFSECTID_SYMTAB
304    3, // ELFSECTID_DYNSTR
305    1, // ELFSECTID_DYNSYM
306    4, // ELFSECTID_TEXT
307    SHN_UNDEF, // ELFSECTID_RODATA
308    SHN_UNDEF, // ELFSECTID_DATA
309    SHN_UNDEF, // ELFSECTID_BSS
310    8, // ELFSECTID_COMMENT
311    2, // ROCMSECTID_HASH
312    5, // ROCMSECTID_DYNAMIC
313    6, // ROCMSECTID_NOTE
314    7 // ROCMSECTID_GPUCONFIG
315};
316
317static const AMDGPUArchValues rocmAmdGpuArchValuesTbl[] =
318{
319    { 0, 0, 0 }, // GPUDeviceType::CAPE_VERDE
320    { 0, 0, 0 }, // GPUDeviceType::PITCAIRN
321    { 0, 0, 0 }, // GPUDeviceType::TAHITI
322    { 0, 0, 0 }, // GPUDeviceType::OLAND
323    { 7, 0, 0 }, // GPUDeviceType::BONAIRE
324    { 7, 0, 0 }, // GPUDeviceType::SPECTRE
325    { 7, 0, 0 }, // GPUDeviceType::SPOOKY
326    { 7, 0, 0 }, // GPUDeviceType::KALINDI
327    { 0, 0, 0 }, // GPUDeviceType::HAINAN
328    { 7, 0, 1 }, // GPUDeviceType::HAWAII
329    { 8, 0, 0 }, // GPUDeviceType::ICELAND
330    { 8, 0, 0 }, // GPUDeviceType::TONGA
331    { 7, 0, 0 }, // GPUDeviceType::MULLINS
332    { 8, 0, 3 }, // GPUDeviceType::FIJI
333    { 8, 0, 1 }, // GPUDeviceType::CARRIZO
334    { 8, 0, 1 }, // GPUDeviceType::DUMMY
335    { 8, 0, 4 }, // GPUDeviceType::GOOSE
336    { 8, 0, 4 }, // GPUDeviceType::HORSE
337    { 8, 0, 1 }, // GPUDeviceType::STONEY
338    { 8, 0, 4 }, // GPUDeviceType::ELLESMERE
339    { 8, 0, 4 }, // GPUDeviceType::BAFFIN
340    { 8, 0, 4 }, // GPUDeviceType::GFX804
341    { 9, 0, 0 }, // GPUDeviceType::GFX900
342    { 9, 0, 1 }  // GPUDeviceType::GFX901
343};
344
345void ROCmBinGenerator::generateInternal(std::ostream* osPtr, std::vector<char>* vPtr,
346             Array<cxbyte>* aPtr) const
347{
348    AMDGPUArchValues amdGpuArchValues = rocmAmdGpuArchValuesTbl[cxuint(input->deviceType)];
349    if (input->archMinor!=UINT32_MAX)
350        amdGpuArchValues.minor = input->archMinor;
351    if (input->archStepping!=UINT32_MAX)
352        amdGpuArchValues.stepping = input->archStepping;
353   
354    const char* comment = "CLRX ROCmBinGenerator " CLRX_VERSION;
355    uint32_t commentSize = ::strlen(comment);
356    if (input->comment!=nullptr)
357    {
358        // if comment, store comment section
359        comment = input->comment;
360        commentSize = input->commentSize;
361        if (commentSize==0)
362            commentSize = ::strlen(comment);
363    }
364   
365    ElfBinaryGen64 elfBinGen64({ 0U, 0U, 0x40, 0, ET_DYN,
366        0xe0, EV_CURRENT, UINT_MAX, 0, 0 }, true, true, true, PHREGION_FILESTART);
367    // add symbols (kernels, function kernels and data symbols)
368    elfBinGen64.addSymbol(ElfSymbol64("_DYNAMIC", 5,
369                  ELF64_ST_INFO(STB_LOCAL, STT_NOTYPE), STV_HIDDEN, true, 0, 0));
370    for (const ROCmSymbolInput& symbol: input->symbols)
371    {
372        ElfSymbol64 elfsym;
373        switch (symbol.type)
374        {
375            case ROCmRegionType::KERNEL:
376                elfsym = ElfSymbol64(symbol.symbolName.c_str(), 4,
377                      ELF64_ST_INFO(STB_GLOBAL, STT_GNU_IFUNC), 0, true,
378                      symbol.offset, symbol.size);
379                break;
380            case ROCmRegionType::FKERNEL:
381                elfsym = ElfSymbol64(symbol.symbolName.c_str(), 4,
382                      ELF64_ST_INFO(STB_GLOBAL, STT_FUNC), 0, true,
383                      symbol.offset, symbol.size);
384                break;
385            case ROCmRegionType::DATA:
386                elfsym = ElfSymbol64(symbol.symbolName.c_str(), 4,
387                      ELF64_ST_INFO(STB_GLOBAL, STT_OBJECT), 0, true,
388                      symbol.offset, symbol.size);
389                break;
390            default:
391                break;
392        }
393        // add to symbols and dynamic symbols table
394        elfBinGen64.addSymbol(elfsym);
395        elfBinGen64.addDynSymbol(elfsym);
396    }
397   
398    static const int32_t dynTags[] = {
399        DT_SYMTAB, DT_SYMENT, DT_STRTAB, DT_STRSZ, DT_HASH };
400    elfBinGen64.addDynamics(sizeof(dynTags)/sizeof(int32_t), dynTags);
401    // elf program headers
402    elfBinGen64.addProgramHeader({ PT_PHDR, PF_R, 0, 1,
403                    true, Elf64Types::nobase, Elf64Types::nobase, 0 });
404    elfBinGen64.addProgramHeader({ PT_LOAD, PF_R, PHREGION_FILESTART, 4,
405                    true, Elf64Types::nobase, Elf64Types::nobase, 0, 0x1000 });
406    elfBinGen64.addProgramHeader({ PT_LOAD, PF_R|PF_X, 4, 1,
407                    true, Elf64Types::nobase, Elf64Types::nobase, 0 });
408    elfBinGen64.addProgramHeader({ PT_LOAD, PF_R|PF_W, 5, 1,
409                    true, Elf64Types::nobase, Elf64Types::nobase, 0 });
410    elfBinGen64.addProgramHeader({ PT_DYNAMIC, PF_R|PF_W, 5, 1,
411                    true, Elf64Types::nobase, Elf64Types::nobase, 0, 8 });
412    elfBinGen64.addProgramHeader({ PT_GNU_RELRO, PF_R, 5, 1,
413                    true, Elf64Types::nobase, Elf64Types::nobase, 0, 1 });
414    elfBinGen64.addProgramHeader({ PT_GNU_STACK, PF_R|PF_W, PHREGION_FILESTART, 0,
415                    true, 0, 0, 0 });
416   
417    // elf notes
418    elfBinGen64.addNote({"AMD", sizeof noteDescType1, noteDescType1, 1U});
419    std::unique_ptr<cxbyte[]> noteBuf(new cxbyte[0x1b]);
420    ::memcpy(noteBuf.get(), noteDescType3, 0x1b);
421    SULEV(*(uint32_t*)(noteBuf.get()+4), amdGpuArchValues.major);
422    SULEV(*(uint32_t*)(noteBuf.get()+8), amdGpuArchValues.minor);
423    SULEV(*(uint32_t*)(noteBuf.get()+12), amdGpuArchValues.stepping);
424    elfBinGen64.addNote({"AMD", 0x1b, noteBuf.get(), 3U});
425   
426    /// region and sections
427    elfBinGen64.addRegion(ElfRegion64::programHeaderTable());
428    elfBinGen64.addRegion(ElfRegion64(0, (const cxbyte*)nullptr, 8,
429                ".dynsym", SHT_DYNSYM, SHF_ALLOC, 0, 1, Elf64Types::nobase));
430    elfBinGen64.addRegion(ElfRegion64(0, (const cxbyte*)nullptr, 4,
431                ".hash", SHT_HASH, SHF_ALLOC, 1, 0, Elf64Types::nobase));
432    elfBinGen64.addRegion(ElfRegion64(0, (const cxbyte*)nullptr, 1, ".dynstr", SHT_STRTAB,
433                SHF_ALLOC, 0, 0, Elf64Types::nobase));
434    // '.text' with alignment=4096
435    elfBinGen64.addRegion(ElfRegion64(input->codeSize, (const cxbyte*)input->code, 
436              0x1000, ".text", SHT_PROGBITS, SHF_ALLOC|SHF_EXECINSTR, 0, 0,
437              Elf64Types::nobase, 0, false, 256));
438    elfBinGen64.addRegion(ElfRegion64(0, (const cxbyte*)nullptr, 0x1000,
439                ".dynamic", SHT_DYNAMIC, SHF_ALLOC|SHF_WRITE, 3, 0,
440                Elf64Types::nobase, 0, false, 8));
441    elfBinGen64.addRegion(ElfRegion64::noteSection());
442    elfBinGen64.addRegion(ElfRegion64(0, (const cxbyte*)nullptr, 1,
443                ".AMDGPU.config", SHT_PROGBITS, 0));
444    elfBinGen64.addRegion(ElfRegion64(commentSize, (const cxbyte*)comment, 1, ".comment",
445              SHT_PROGBITS, SHF_MERGE|SHF_STRINGS, 0, 0, 0, 1));
446    elfBinGen64.addRegion(ElfRegion64(0, (const cxbyte*)nullptr, 8,
447                ".symtab", SHT_SYMTAB, 0, 0, 1));
448    elfBinGen64.addRegion(ElfRegion64::shstrtabSection());
449    elfBinGen64.addRegion(ElfRegion64::strtabSection());
450    elfBinGen64.addRegion(ElfRegion64::sectionHeaderTable());
451   
452    /* extra sections */
453    for (const BinSection& section: input->extraSections)
454        elfBinGen64.addRegion(ElfRegion64(section, mainBuiltinSectionTable,
455                         ROCMSECTID_MAX, 12));
456    /* extra symbols */
457    for (const BinSymbol& symbol: input->extraSymbols)
458        elfBinGen64.addSymbol(ElfSymbol64(symbol, mainBuiltinSectionTable,
459                         ROCMSECTID_MAX, 12));
460   
461    size_t binarySize = elfBinGen64.countSize();
462    /****
463     * prepare for write binary to output
464     ****/
465    std::unique_ptr<std::ostream> outStreamHolder;
466    std::ostream* os = nullptr;
467    if (aPtr != nullptr)
468    {
469        aPtr->resize(binarySize);
470        outStreamHolder.reset(
471                new ArrayOStream(binarySize, reinterpret_cast<char*>(aPtr->data())));
472        os = outStreamHolder.get();
473    }
474    else if (vPtr != nullptr)
475    {
476        vPtr->resize(binarySize);
477        outStreamHolder.reset(new VectorOStream(*vPtr));
478        os = outStreamHolder.get();
479    }
480    else // from argument
481        os = osPtr;
482   
483    const std::ios::iostate oldExceptions = os->exceptions();
484    try
485    {
486    os->exceptions(std::ios::failbit | std::ios::badbit);
487    /****
488     * write binary to output
489     ****/
490    FastOutputBuffer bos(256, *os);
491    elfBinGen64.generate(bos);
492    assert(bos.getWritten() == binarySize);
493    }
494    catch(...)
495    {
496        os->exceptions(oldExceptions);
497        throw;
498    }
499    os->exceptions(oldExceptions);
500}
501
502void ROCmBinGenerator::generate(Array<cxbyte>& array) const
503{
504    generateInternal(nullptr, nullptr, &array);
505}
506
507void ROCmBinGenerator::generate(std::ostream& os) const
508{
509    generateInternal(&os, nullptr, nullptr);
510}
511
512void ROCmBinGenerator::generate(std::vector<char>& v) const
513{
514    generateInternal(nullptr, &v, nullptr);
515}
Note: See TracBrowser for help on using the repository browser.