pinokiod 8.0.45 → 8.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kernel/sysinfo.js CHANGED
@@ -3,6 +3,7 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
  const nvidia = require("./gpu/nvidia")
5
5
  const amd = require("./gpu/amd")
6
+ const amd_pci = require("./gpu/amd_pci")
6
7
  class Sysinfo {
7
8
  async init(kernel) {
8
9
  this.kernel = kernel
@@ -139,7 +140,11 @@ class Sysinfo {
139
140
  if (detected.is_nvidia) {
140
141
  return await nvidia.resolve_cuda_sm_target(detected.primaryController)
141
142
  } else if (detected.is_amd) {
142
- return await amd.resolve_gpu_target(detected.gpu_model, detected.cpu_brand || (() => this.cpu_brand()))
143
+ let target = await amd.resolve_gpu_target(
144
+ detected.gpu_model,
145
+ detected.cpu_brand || (() => this.cpu_brand())
146
+ )
147
+ return target || await amd_pci.resolve_gpu_target(detected.primaryController)
143
148
  } else {
144
149
  return null
145
150
  }
@@ -5,5 +5,6 @@ module.exports = {
5
5
  ENTRY_BATCH_SIZE: 256,
6
6
  DIR_CONCURRENCY: 8,
7
7
  STAT_CONCURRENCY: 32,
8
- HASH_QUEUE_LIMIT: 256
8
+ HASH_QUEUE_LIMIT: 256,
9
+ HASH_INACTIVITY_MS: 120 * 1000
9
10
  }
@@ -6,20 +6,32 @@ const fs = require('fs')
6
6
  // events. This changes only the read granularity; every byte still feeds the
7
7
  // same sha256 digest, one file at a time.
8
8
  const HASH_READ_SIZE = 1024 * 1024
9
+ const HASH_PROGRESS_INTERVAL_MS = 1000
9
10
 
10
11
  parentPort.on('message', ({ id, filePath }) => {
11
12
  const hash = crypto.createHash('sha256')
12
13
  let size = 0
14
+ let lastProgressAt = 0
15
+ let complete = false
13
16
  const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
14
17
  const stream = fs.createReadStream(filePath, { flags, highWaterMark: HASH_READ_SIZE })
15
18
  stream.on('data', (chunk) => {
16
19
  size += chunk.length
17
20
  hash.update(chunk)
21
+ const now = Date.now()
22
+ if (now - lastProgressAt >= HASH_PROGRESS_INTERVAL_MS) {
23
+ lastProgressAt = now
24
+ parentPort.postMessage({ id, bytes_read: size })
25
+ }
18
26
  })
19
27
  stream.on('error', (error) => {
20
- parentPort.postMessage({ id, error: error.message })
28
+ if (complete) return
29
+ complete = true
30
+ parentPort.postMessage({ id, error: error.message, code: error.code })
21
31
  })
22
32
  stream.on('end', () => {
33
+ if (complete) return
34
+ complete = true
23
35
  parentPort.postMessage({ id, hash: hash.digest('hex'), size })
24
36
  })
25
37
  })
@@ -6,7 +6,10 @@ const Registry = require('./registry')
6
6
  const Sweeper = require('./sweeper')
7
7
  const { walkBatches, statMany } = require('./walker')
8
8
  const { fileSnapshot, sameSnapshot, sameContentState } = require('./snapshot')
9
- const { SIZE_THRESHOLD, TMP_SUFFIX, SHA256_RE, DIR_CONCURRENCY, STAT_CONCURRENCY } = require('./constants')
9
+ const {
10
+ SIZE_THRESHOLD, TMP_SUFFIX, SHA256_RE, DIR_CONCURRENCY, STAT_CONCURRENCY,
11
+ HASH_INACTIVITY_MS
12
+ } = require('./constants')
10
13
 
11
14
  // Shared model store engine (spec/requirements/shared-model-store.md).
12
15
  // Store + registry + volume probe + hashing + adopt/convert/verify, plus the
@@ -81,6 +84,7 @@ class Vault {
81
84
  this.workerSeq = 0
82
85
  this.workerIdleTimer = null
83
86
  this.workerIdleMs = 750
87
+ this.hashInactivityMs = HASH_INACTIVITY_MS
84
88
  this.statConcurrency = STAT_CONCURRENCY
85
89
  this.dirConcurrency = DIR_CONCURRENCY
86
90
  this.sizeThreshold = SIZE_THRESHOLD
@@ -675,7 +679,21 @@ class Vault {
675
679
  this.volumeModes.set(dev, mode)
676
680
  return mode
677
681
  }
678
- async hashFile(filePath) {
682
+ failHashWorker(worker, error, terminate = false) {
683
+ if (this.worker === worker) {
684
+ if (this.workerIdleTimer) clearTimeout(this.workerIdleTimer)
685
+ this.workerIdleTimer = null
686
+ this.worker = null
687
+ }
688
+ for (const [id, job] of [...this.workerJobs]) {
689
+ if (job.worker !== worker) continue
690
+ clearTimeout(job.inactivityTimer)
691
+ this.workerJobs.delete(id)
692
+ job.reject(error)
693
+ }
694
+ if (terminate) worker.terminate().catch(() => {})
695
+ }
696
+ async hashFile(filePath, options = {}) {
679
697
  if (this.workerIdleTimer) {
680
698
  clearTimeout(this.workerIdleTimer)
681
699
  this.workerIdleTimer = null
@@ -684,12 +702,24 @@ class Vault {
684
702
  const worker = new Worker(path.resolve(__dirname, "hash_worker.js"))
685
703
  this.worker = worker
686
704
  worker.unref()
687
- worker.on("message", ({ id, hash, size, error }) => {
705
+ worker.on("message", ({ id, hash, size, bytes_read: bytesRead, error, code }) => {
688
706
  const job = this.workerJobs.get(id)
689
- if (!job) return
707
+ if (!job || job.worker !== worker) return
708
+ if (Number.isFinite(bytesRead)) {
709
+ job.resetInactivity()
710
+ job.reportProgress(bytesRead)
711
+ return
712
+ }
713
+ clearTimeout(job.inactivityTimer)
690
714
  this.workerJobs.delete(id)
691
- if (error) job.reject(new Error(error))
692
- else job.resolve({ hash, size })
715
+ if (error) {
716
+ const failure = new Error(error)
717
+ if (code) failure.code = code
718
+ job.reject(failure)
719
+ } else {
720
+ job.reportProgress(size)
721
+ job.resolve({ hash, size })
722
+ }
693
723
  // Keep the worker warm across the scan queue. Starting one worker per
694
724
  // file costs ~30ms and adds up quickly on a first scan.
695
725
  if (this.workerJobs.size === 0 && this.worker === worker) {
@@ -705,27 +735,46 @@ class Vault {
705
735
  })
706
736
  worker.on("error", (error) => {
707
737
  if (this.worker !== worker) return
708
- if (this.workerIdleTimer) clearTimeout(this.workerIdleTimer)
709
- this.workerIdleTimer = null
710
- for (const job of this.workerJobs.values()) job.reject(error)
711
- this.workerJobs.clear()
712
- this.worker = null
738
+ this.failHashWorker(worker, error)
713
739
  })
714
740
  worker.on("exit", (code) => {
715
741
  if (this.worker !== worker) return
716
- if (this.workerIdleTimer) clearTimeout(this.workerIdleTimer)
717
- this.workerIdleTimer = null
718
- for (const job of this.workerJobs.values()) {
719
- job.reject(new Error(`hash worker exited with code ${code}`))
720
- }
721
- this.workerJobs.clear()
722
- this.worker = null
742
+ this.failHashWorker(worker, new Error(`hash worker exited with code ${code}`))
723
743
  })
724
744
  }
745
+ const worker = this.worker
725
746
  const id = ++this.workerSeq
726
747
  return new Promise((resolve, reject) => {
727
- this.workerJobs.set(id, { resolve, reject })
728
- this.worker.postMessage({ id, filePath })
748
+ const onProgress = typeof options.onProgress === "function" ? options.onProgress : null
749
+ const job = {
750
+ worker,
751
+ resolve,
752
+ reject,
753
+ reportProgress: (bytes) => {
754
+ if (!onProgress) return
755
+ // Progress is observational and must never fail a content hash.
756
+ try { onProgress(bytes) } catch (_) {}
757
+ },
758
+ inactivityTimer: null,
759
+ resetInactivity: null
760
+ }
761
+ job.resetInactivity = () => {
762
+ clearTimeout(job.inactivityTimer)
763
+ const inactivityMs = Math.max(1, Number(this.hashInactivityMs) || HASH_INACTIVITY_MS)
764
+ job.inactivityTimer = setTimeout(() => {
765
+ const failure = new Error(`Timed out while reading ${path.basename(filePath)}`)
766
+ failure.code = "ETIMEDOUT"
767
+ this.failHashWorker(worker, failure, true)
768
+ }, inactivityMs)
769
+ if (job.inactivityTimer.unref) job.inactivityTimer.unref()
770
+ }
771
+ this.workerJobs.set(id, job)
772
+ job.resetInactivity()
773
+ try {
774
+ worker.postMessage({ id, filePath })
775
+ } catch (error) {
776
+ this.failHashWorker(worker, error, true)
777
+ }
729
778
  })
730
779
  }
731
780
  async refreshLinkSnapshots(hash, dev, ino) {
@@ -1798,6 +1847,8 @@ class Vault {
1798
1847
  : sweeper.state
1799
1848
  return Object.assign({}, state, {
1800
1849
  current_file: sweeper.currentHash ? path.basename(sweeper.currentHash.path) : null,
1850
+ current_file_bytes: sweeper.currentHash ? sweeper.currentHash.bytes : null,
1851
+ current_file_size: sweeper.currentHash ? sweeper.currentHash.size : null,
1801
1852
  pending,
1802
1853
  error: this.scanError
1803
1854
  })
@@ -54,12 +54,11 @@ class Sweeper {
54
54
  idleState() {
55
55
  return {
56
56
  active: false, phase: "idle", dirs: 0, files: 0, bytes_total: 0,
57
- counted_dirs: 0, counted_files: 0, total_files: null, count_estimate_files: null,
57
+ counted_dirs: 0, counted_files: 0, total_files: null,
58
58
  home_bytes_total: 0, source_bytes: {}, source_files: {}, source_hash_failures: {}, scope_id: null,
59
59
  candidates: 0, hashed: 0, hash_total: 0, hash_bytes: 0, queued: 0,
60
60
  inode_reuses: 0, unstable_hashes: 0, hash_failures: 0,
61
61
  inaccessible: 0, inaccessible_paths: [],
62
- estimated_count_weight: 0.4, estimated_walk_weight: 0.5,
63
62
  started: null, duration_ms: null,
64
63
  count_duration_ms: null, walk_duration_ms: null, hash_wait_duration_ms: null,
65
64
  hash_duration_ms: 0
@@ -84,24 +83,6 @@ class Sweeper {
84
83
  if (!scopeId) this.vault.reconcileConfiguredSources()
85
84
  const scanRoots = this.vault.scanRoots(scopeId)
86
85
  if (!scanRoots.length) throw new Error("That scan location is no longer available.")
87
- const previous = this.vault.registry.scanFor(scopeId)
88
- if (previous) {
89
- if (Number.isFinite(previous.files) && previous.files > 0) {
90
- this.state.count_estimate_files = previous.files
91
- }
92
- const duration = Math.max(0, Number(previous.duration_ms) || 0)
93
- const walkDuration = Math.max(0, Number(previous.walk_duration_ms) || 0)
94
- const waitDuration = Math.max(0, Number(previous.hash_wait_duration_ms) || 0)
95
- const countDuration = Number.isFinite(previous.count_duration_ms)
96
- ? Math.max(0, previous.count_duration_ms)
97
- : Math.max(0, duration - walkDuration - waitDuration)
98
- const finishDuration = Math.max(1, duration - countDuration - walkDuration)
99
- const measuredDuration = countDuration + walkDuration + finishDuration
100
- if (measuredDuration > 1) {
101
- this.state.estimated_count_weight = countDuration / measuredDuration
102
- this.state.estimated_walk_weight = walkDuration / measuredDuration
103
- }
104
- }
105
86
  const countStarted = Date.now()
106
87
  for (const source of scanRoots) await this.countFiles(source.root)
107
88
  this.state.count_duration_ms = Date.now() - countStarted
@@ -411,9 +392,20 @@ class Sweeper {
411
392
  async _hashJob(job) {
412
393
  const primary = job.names[0]
413
394
  const started = Date.now()
414
- this.currentHash = { path: primary.filePath }
395
+ const currentHash = {
396
+ path: primary.filePath,
397
+ size: Math.max(0, Number(primary.expected.size) || 0),
398
+ bytes: 0
399
+ }
400
+ this.currentHash = currentHash
415
401
  try {
416
- const { hash, size } = await this.vault.hashFile(primary.filePath)
402
+ const { hash, size } = await this.vault.hashFile(primary.filePath, {
403
+ onProgress: (bytes) => {
404
+ if (this.currentHash === currentHash) {
405
+ currentHash.bytes = Math.min(currentHash.size, Math.max(0, Number(bytes) || 0))
406
+ }
407
+ }
408
+ })
417
409
  this.state.hashed += 1
418
410
  this.state.hash_bytes += size || 0
419
411
  const primaryStat = await fs.promises.lstat(primary.filePath)
@@ -460,7 +452,7 @@ class Sweeper {
460
452
  if (job.inodeKey && this.hashJobsByIno.get(job.inodeKey) === job) {
461
453
  this.hashJobsByIno.delete(job.inodeKey)
462
454
  }
463
- this.currentHash = null
455
+ if (this.currentHash === currentHash) this.currentHash = null
464
456
  this.state.queued = Math.max(0, this.state.queued - 1)
465
457
  if (this.state.queued < this.hashQueueLimit) {
466
458
  for (const resolve of this.hashCapacityWaiters.splice(0)) resolve()
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.45",
3
+ "version": "8.0.46",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "download_readme": "wget -O kernel/proto/PINOKIO.md https://raw.githubusercontent.com/pinokiocomputer/home/refs/heads/main/docs/README.md",
8
8
  "update:amd-gfx-targets": "node script/update-amd-gfx-targets.js",
9
+ "check:amd-gfx-targets": "node script/update-amd-gfx-targets.js --check",
9
10
  "start": "node script/index"
10
11
  },
11
12
  "author": "",
@@ -1,11 +1,65 @@
1
+ const crypto = require('node:crypto')
1
2
  const fs = require('node:fs')
2
3
  const https = require('node:https')
3
4
  const path = require('node:path')
4
5
 
5
6
  const { normalize_model } = require('../kernel/gpu/common')
6
7
 
7
- const GPU_SPECS_URL = 'https://raw.githubusercontent.com/ROCm/ROCm/develop/docs/reference/gpu-arch-specs.rst'
8
8
  const GENERATOR = 'script/update-amd-gfx-targets.js'
9
+ const AMD_VENDOR_ID = '1002'
10
+
11
+ // Update these revisions deliberately during release maintenance. Runtime GPU
12
+ // detection never fetches upstream data.
13
+ const ROCM_REVISION = '14f8138863403a26e0caef6671cfab9b09aa636e'
14
+ const MESA_REVISION = '3e2d8517b897026377267c09975db83525d2fc95'
15
+ const LLVM_REVISION = '2f3f97fb72606f51ece7d64d6a7a73c9eaad8978'
16
+ const LINUX_REVISION = '7d0a66e4bb9081d75c82ec4957c50034cb0ea449'
17
+ const LIBDRM_REVISION = 'f198c21dfcb89127083413dd4779a13b3f9a6507'
18
+
19
+ const SOURCES = {
20
+ rocm_specs: {
21
+ repository: 'https://github.com/ROCm/ROCm',
22
+ revision: ROCM_REVISION,
23
+ path: 'docs/reference/gpu-arch-specs.rst',
24
+ url: `https://raw.githubusercontent.com/ROCm/ROCm/${ROCM_REVISION}/docs/reference/gpu-arch-specs.rst`
25
+ },
26
+ mesa_pci_ids: {
27
+ repository: 'https://gitlab.freedesktop.org/mesa/mesa',
28
+ revision: MESA_REVISION,
29
+ path: 'include/pci_ids/radeonsi_pci_ids.h',
30
+ url: `https://gitlab.freedesktop.org/mesa/mesa/-/raw/${MESA_REVISION}/include/pci_ids/radeonsi_pci_ids.h`
31
+ },
32
+ mesa_families: {
33
+ repository: 'https://gitlab.freedesktop.org/mesa/mesa',
34
+ revision: MESA_REVISION,
35
+ path: 'src/amd/common/amd_family.c',
36
+ url: `https://gitlab.freedesktop.org/mesa/mesa/-/raw/${MESA_REVISION}/src/amd/common/amd_family.c`
37
+ },
38
+ mesa_family_names: {
39
+ repository: 'https://gitlab.freedesktop.org/mesa/mesa',
40
+ revision: MESA_REVISION,
41
+ path: 'src/amd/common/amd_family.h',
42
+ url: `https://gitlab.freedesktop.org/mesa/mesa/-/raw/${MESA_REVISION}/src/amd/common/amd_family.h`
43
+ },
44
+ llvm_processors: {
45
+ repository: 'https://github.com/llvm/llvm-project',
46
+ revision: LLVM_REVISION,
47
+ path: 'llvm/lib/Target/AMDGPU/GCNProcessors.td',
48
+ url: `https://raw.githubusercontent.com/llvm/llvm-project/${LLVM_REVISION}/llvm/lib/Target/AMDGPU/GCNProcessors.td`
49
+ },
50
+ linux_pci_ids: {
51
+ repository: 'https://github.com/torvalds/linux',
52
+ revision: LINUX_REVISION,
53
+ path: 'drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c',
54
+ url: `https://raw.githubusercontent.com/torvalds/linux/${LINUX_REVISION}/drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c`
55
+ },
56
+ libdrm_names: {
57
+ repository: 'https://gitlab.freedesktop.org/mesa/libdrm',
58
+ revision: LIBDRM_REVISION,
59
+ path: 'data/amdgpu.ids',
60
+ url: `https://gitlab.freedesktop.org/mesa/libdrm/-/raw/${LIBDRM_REVISION}/data/amdgpu.ids`
61
+ }
62
+ }
9
63
 
10
64
  const repoRoot = path.resolve(__dirname, '..')
11
65
 
@@ -33,6 +87,10 @@ function fetchText(url) {
33
87
  })
34
88
  }
35
89
 
90
+ function sha256(value) {
91
+ return crypto.createHash('sha256').update(value).digest('hex')
92
+ }
93
+
36
94
  function canonicalKey(value) {
37
95
  let key = normalize_model(value)
38
96
  let previous
@@ -95,25 +153,243 @@ function parseGpuSpecs(rst) {
95
153
  return entries
96
154
  }
97
155
 
156
+ function parseLlvmProcessorTargets(tablegen) {
157
+ const processors = {}
158
+ const processorPattern = /def\s*:\s*ProcessorModel<"([^"]+)"([\s\S]*?)>;/g
159
+ let processorMatch
160
+ while ((processorMatch = processorPattern.exec(tablegen))) {
161
+ const name = processorMatch[1].toLowerCase()
162
+ const version = processorMatch[2].match(
163
+ /FeatureISAVersion([0-9]+)_([0-9a-f]+)_([0-9a-f]+)\.Features/i
164
+ )
165
+ if (!version) continue
166
+ const target = /^gfx[0-9a-f]+$/.test(name)
167
+ ? name
168
+ : `gfx${version[1]}${version[2]}${version[3]}`.toLowerCase()
169
+ processors[name] = target
170
+ }
171
+ return processors
172
+ }
173
+
174
+ function parseMesaFamilyTargets(source, processorTargets) {
175
+ const start = source.indexOf('const char *ac_get_llvm_processor_name')
176
+ const end = source.indexOf('const char *ac_get_ip_type_string', start)
177
+ if (start < 0 || end < 0) {
178
+ throw new Error('Mesa ac_get_llvm_processor_name function was not found')
179
+ }
180
+
181
+ const body = source.slice(start, end)
182
+ const familyTargets = {}
183
+ const returnPattern = /((?:\s*case\s+CHIP_[A-Z0-9_]+:\s*)+)\s*return\s+"([^"]+)";/g
184
+ let returnMatch
185
+ while ((returnMatch = returnPattern.exec(body))) {
186
+ const processor = returnMatch[2].toLowerCase()
187
+ const target = /^gfx[0-9a-f]+$/.test(processor)
188
+ ? processor
189
+ : processorTargets[processor]
190
+ if (!target || !/^gfx[0-9a-f]+$/.test(target)) continue
191
+
192
+ const familyPattern = /case\s+CHIP_([A-Z0-9_]+):/g
193
+ let familyMatch
194
+ while ((familyMatch = familyPattern.exec(returnMatch[1]))) {
195
+ familyTargets[familyMatch[1]] = target
196
+ }
197
+ }
198
+ return familyTargets
199
+ }
200
+
201
+ function parseMesaFamilyAliases(source) {
202
+ const aliases = {}
203
+ const aliasPattern = /CHIP_([A-Z0-9_]+),[^\n]*formerly\s+"([^"]+)"/gi
204
+ let aliasMatch
205
+ while ((aliasMatch = aliasPattern.exec(source))) {
206
+ const current = aliasMatch[1].toUpperCase()
207
+ const former = aliasMatch[2].toUpperCase().replace(/[^A-Z0-9]+/g, '_')
208
+ aliases[former] = current
209
+ }
210
+ return aliases
211
+ }
212
+
213
+ function addPciEntry(entries, device, family, target) {
214
+ if (!target) return
215
+ const key = `${AMD_VENDOR_ID}:${device.toLowerCase()}`
216
+ const previous = entries[key]
217
+ if (previous && previous.target !== target) {
218
+ throw new Error(
219
+ `Conflicting targets for ${key}: ${previous.target} (${previous.family}) and ${target} (${family})`
220
+ )
221
+ }
222
+ entries[key] = { target, family }
223
+ }
224
+
225
+ function parseMesaPciTargets(source, familyTargets, entries = {}) {
226
+ const pciPattern = /CHIPSET\(0x([0-9a-f]{4}),\s*([A-Z0-9_]+)\)/gi
227
+ let pciMatch
228
+ while ((pciMatch = pciPattern.exec(source))) {
229
+ const device = pciMatch[1].toLowerCase()
230
+ const family = pciMatch[2].toUpperCase()
231
+ addPciEntry(entries, device, family, familyTargets[family])
232
+ }
233
+ return entries
234
+ }
235
+
236
+ function parseLinuxPciTargets(source, familyTargets, familyAliases, entries = {}) {
237
+ const pciPattern = /\{\s*0x1002,\s*0x([0-9a-f]{4}),[^\n]*\bCHIP_([A-Z0-9_]+)/gi
238
+ let pciMatch
239
+ while ((pciMatch = pciPattern.exec(source))) {
240
+ const device = pciMatch[1].toLowerCase()
241
+ const sourceFamily = pciMatch[2].toUpperCase()
242
+ const family = familyAliases[sourceFamily] || sourceFamily
243
+ addPciEntry(entries, device, family, familyTargets[family])
244
+ }
245
+ return entries
246
+ }
247
+
248
+ function parseLibdrmRevisionTargets(source, modelTargets, pciTargets) {
249
+ const entries = {}
250
+ const rowPattern = /^([0-9a-f]{4}),\s*([0-9a-f]{2}),\s*(.+?)\s*$/gim
251
+ let rowMatch
252
+ while ((rowMatch = rowPattern.exec(source))) {
253
+ const device = rowMatch[1].toLowerCase()
254
+ const revision = rowMatch[2].toLowerCase()
255
+ const model = rowMatch[3].trim()
256
+ const modelTarget = modelTargets[canonicalKey(model)]
257
+ const pciTarget = pciTargets[`${AMD_VENDOR_ID}:${device}`]
258
+ if (pciTarget && modelTarget && pciTarget.target !== modelTarget) {
259
+ throw new Error(
260
+ `Conflicting family/model targets for ${device}:${revision} (${model}): ` +
261
+ `${pciTarget.target} and ${modelTarget}`
262
+ )
263
+ }
264
+ const target = pciTarget ? pciTarget.target : modelTarget
265
+ if (target) {
266
+ entries[`${AMD_VENDOR_ID}:${device}:${revision}`] = { target, model }
267
+ }
268
+ }
269
+ return entries
270
+ }
271
+
272
+ function sourceMetadata(definition, contents) {
273
+ return {
274
+ repository: definition.repository,
275
+ revision: definition.revision,
276
+ path: definition.path,
277
+ url: definition.url,
278
+ sha256: sha256(contents)
279
+ }
280
+ }
281
+
282
+ function sortedObject(entries) {
283
+ return Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
284
+ }
285
+
286
+ function buildOutputs(contents) {
287
+ const modelTargets = parseGpuSpecs(contents.rocm_specs)
288
+ const processorTargets = parseLlvmProcessorTargets(contents.llvm_processors)
289
+ const familyTargets = parseMesaFamilyTargets(contents.mesa_families, processorTargets)
290
+ const familyAliases = parseMesaFamilyAliases(contents.mesa_family_names)
291
+ const pciTargets = parseMesaPciTargets(contents.mesa_pci_ids, familyTargets)
292
+ parseLinuxPciTargets(
293
+ contents.linux_pci_ids,
294
+ familyTargets,
295
+ familyAliases,
296
+ pciTargets
297
+ )
298
+ const revisionTargets = parseLibdrmRevisionTargets(
299
+ contents.libdrm_names,
300
+ modelTargets,
301
+ pciTargets
302
+ )
303
+
304
+ const required = {
305
+ '1002:731f': 'gfx1010',
306
+ '1002:7340': 'gfx1012',
307
+ '1002:73bf': 'gfx1030',
308
+ '1002:73ff': 'gfx1032'
309
+ }
310
+ for (const [key, target] of Object.entries(required)) {
311
+ if (!pciTargets[key] || pciTargets[key].target !== target) {
312
+ throw new Error(`Pinned sources did not produce required mapping ${key} -> ${target}`)
313
+ }
314
+ }
315
+
316
+ return {
317
+ models: {
318
+ source: SOURCES.rocm_specs.url,
319
+ source_revision: SOURCES.rocm_specs.revision,
320
+ source_sha256: sha256(contents.rocm_specs),
321
+ generated_by: GENERATOR,
322
+ entries: sortedObject(modelTargets)
323
+ },
324
+ pci: {
325
+ generated_by: GENERATOR,
326
+ sources: [
327
+ sourceMetadata(SOURCES.mesa_pci_ids, contents.mesa_pci_ids),
328
+ sourceMetadata(SOURCES.mesa_families, contents.mesa_families),
329
+ sourceMetadata(SOURCES.mesa_family_names, contents.mesa_family_names),
330
+ sourceMetadata(SOURCES.llvm_processors, contents.llvm_processors),
331
+ sourceMetadata(SOURCES.linux_pci_ids, contents.linux_pci_ids),
332
+ sourceMetadata(SOURCES.libdrm_names, contents.libdrm_names)
333
+ ],
334
+ revision_entries: sortedObject(revisionTargets),
335
+ entries: sortedObject(pciTargets)
336
+ }
337
+ }
338
+ }
339
+
98
340
  function writeJson(relativePath, data) {
99
341
  const file = path.join(repoRoot, relativePath)
100
342
  fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
101
343
  }
102
344
 
345
+ function checkJson(relativePath, data) {
346
+ const file = path.join(repoRoot, relativePath)
347
+ const expected = `${JSON.stringify(data, null, 2)}\n`
348
+ if (fs.readFileSync(file, 'utf8') !== expected) {
349
+ throw new Error(`${relativePath} is stale; run npm run update:amd-gfx-targets`)
350
+ }
351
+ }
352
+
103
353
  async function main() {
104
- const specs = await fetchText(GPU_SPECS_URL)
105
- const entries = parseGpuSpecs(specs)
354
+ const contents = Object.fromEntries(await Promise.all(
355
+ Object.entries(SOURCES).map(async ([name, source]) => [name, await fetchText(source.url)])
356
+ ))
357
+ const outputs = buildOutputs(contents)
358
+ const files = [
359
+ ['kernel/gpu/amd_gfx_targets.json', outputs.models],
360
+ ['kernel/gpu/amd_pci_targets.json', outputs.pci]
361
+ ]
106
362
 
107
- writeJson('kernel/gpu/amd_gfx_targets.json', {
108
- source: GPU_SPECS_URL,
109
- generated_by: GENERATOR,
110
- entries: Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
111
- })
363
+ if (process.argv.includes('--check')) {
364
+ for (const [file, data] of files) checkJson(file, data)
365
+ console.log('AMD gfx target data is up to date')
366
+ return
367
+ }
112
368
 
113
- console.log(`Wrote ${Object.keys(entries).length} AMD gfx target entries`)
369
+ for (const [file, data] of files) writeJson(file, data)
370
+
371
+ console.log(
372
+ `Wrote ${Object.keys(outputs.models.entries).length} AMD model targets and ` +
373
+ `${Object.keys(outputs.pci.entries).length} AMD PCI targets`
374
+ )
114
375
  }
115
376
 
116
- main().catch((error) => {
117
- console.error(error)
118
- process.exit(1)
119
- })
377
+ if (require.main === module) {
378
+ main().catch((error) => {
379
+ console.error(error)
380
+ process.exit(1)
381
+ })
382
+ }
383
+
384
+ module.exports = {
385
+ SOURCES,
386
+ buildOutputs,
387
+ checkJson,
388
+ parseGpuSpecs,
389
+ parseLibdrmRevisionTargets,
390
+ parseLinuxPciTargets,
391
+ parseLlvmProcessorTargets,
392
+ parseMesaFamilyAliases,
393
+ parseMesaFamilyTargets,
394
+ parseMesaPciTargets
395
+ }