pinokiod 8.0.45 → 8.0.47

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
  }
@@ -1,9 +1,16 @@
1
+ const CANDIDATE_SIZE_BASE = process.platform === "win32" ? 1024 : 1000
2
+ const CANDIDATE_SIZE_OPTIONS = [10, 50, 100, 500]
3
+ .map((value) => value * CANDIDATE_SIZE_BASE ** 2)
4
+ .concat(CANDIDATE_SIZE_BASE ** 3)
5
+
1
6
  module.exports = {
2
- SIZE_THRESHOLD: 100 * 1024 * 1024,
7
+ SIZE_THRESHOLD: CANDIDATE_SIZE_OPTIONS[2],
8
+ CANDIDATE_SIZE_OPTIONS,
3
9
  TMP_SUFFIX: ".pinokio-dedup-tmp",
4
10
  SHA256_RE: /^[0-9a-f]{64}$/,
5
11
  ENTRY_BATCH_SIZE: 256,
6
12
  DIR_CONCURRENCY: 8,
7
13
  STAT_CONCURRENCY: 32,
8
- HASH_QUEUE_LIMIT: 256
14
+ HASH_QUEUE_LIMIT: 256,
15
+ HASH_INACTIVITY_MS: 120 * 1000
9
16
  }
@@ -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, CANDIDATE_SIZE_OPTIONS, TMP_SUFFIX, SHA256_RE,
11
+ DIR_CONCURRENCY, STAT_CONCURRENCY, 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
@@ -167,9 +171,10 @@ class Vault {
167
171
  if (this.fileActionProgress === progress) this.fileActionProgress = null
168
172
  }
169
173
  }
170
- startScan(scopeId = null) {
174
+ startScan(scopeId = null, sizeThreshold = this.sizeThreshold) {
171
175
  if (!this.enabled || !this.sweeper) return { started: false, disabled: true }
172
176
  if (this.scanPromise) return { started: false, already_running: true }
177
+ this.sizeThreshold = sizeThreshold
173
178
  this.scanError = null
174
179
  this.scanScopeId = scopeId
175
180
  this.scanPromise = this.runExclusive(() => this.sweeper.scan(scopeId))
@@ -200,11 +205,19 @@ class Vault {
200
205
  }
201
206
  }
202
207
  }
203
- case "scan":
208
+ case "scan": {
204
209
  if (payload.scope_id && !this.scanSource(payload.scope_id)) {
205
210
  return { error: "That scan location is no longer available." }
206
211
  }
207
- return this.startScan(payload.scope_id || null)
212
+ let sizeThreshold = this.sizeThreshold
213
+ if (payload.candidate_size != null) {
214
+ if (!CANDIDATE_SIZE_OPTIONS.includes(payload.candidate_size)) {
215
+ return { error: "Choose a valid minimum file size." }
216
+ }
217
+ sizeThreshold = payload.candidate_size
218
+ }
219
+ return this.startScan(payload.scope_id || null, sizeThreshold)
220
+ }
208
221
  case "deduplicate":
209
222
  if (typeof payload.path === "string" && payload.path) {
210
223
  return this.runMutation(() => this.runFileAction({
@@ -675,7 +688,21 @@ class Vault {
675
688
  this.volumeModes.set(dev, mode)
676
689
  return mode
677
690
  }
678
- async hashFile(filePath) {
691
+ failHashWorker(worker, error, terminate = false) {
692
+ if (this.worker === worker) {
693
+ if (this.workerIdleTimer) clearTimeout(this.workerIdleTimer)
694
+ this.workerIdleTimer = null
695
+ this.worker = null
696
+ }
697
+ for (const [id, job] of [...this.workerJobs]) {
698
+ if (job.worker !== worker) continue
699
+ clearTimeout(job.inactivityTimer)
700
+ this.workerJobs.delete(id)
701
+ job.reject(error)
702
+ }
703
+ if (terminate) worker.terminate().catch(() => {})
704
+ }
705
+ async hashFile(filePath, options = {}) {
679
706
  if (this.workerIdleTimer) {
680
707
  clearTimeout(this.workerIdleTimer)
681
708
  this.workerIdleTimer = null
@@ -684,12 +711,24 @@ class Vault {
684
711
  const worker = new Worker(path.resolve(__dirname, "hash_worker.js"))
685
712
  this.worker = worker
686
713
  worker.unref()
687
- worker.on("message", ({ id, hash, size, error }) => {
714
+ worker.on("message", ({ id, hash, size, bytes_read: bytesRead, error, code }) => {
688
715
  const job = this.workerJobs.get(id)
689
- if (!job) return
716
+ if (!job || job.worker !== worker) return
717
+ if (Number.isFinite(bytesRead)) {
718
+ job.resetInactivity()
719
+ job.reportProgress(bytesRead)
720
+ return
721
+ }
722
+ clearTimeout(job.inactivityTimer)
690
723
  this.workerJobs.delete(id)
691
- if (error) job.reject(new Error(error))
692
- else job.resolve({ hash, size })
724
+ if (error) {
725
+ const failure = new Error(error)
726
+ if (code) failure.code = code
727
+ job.reject(failure)
728
+ } else {
729
+ job.reportProgress(size)
730
+ job.resolve({ hash, size })
731
+ }
693
732
  // Keep the worker warm across the scan queue. Starting one worker per
694
733
  // file costs ~30ms and adds up quickly on a first scan.
695
734
  if (this.workerJobs.size === 0 && this.worker === worker) {
@@ -705,27 +744,46 @@ class Vault {
705
744
  })
706
745
  worker.on("error", (error) => {
707
746
  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
747
+ this.failHashWorker(worker, error)
713
748
  })
714
749
  worker.on("exit", (code) => {
715
750
  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
751
+ this.failHashWorker(worker, new Error(`hash worker exited with code ${code}`))
723
752
  })
724
753
  }
754
+ const worker = this.worker
725
755
  const id = ++this.workerSeq
726
756
  return new Promise((resolve, reject) => {
727
- this.workerJobs.set(id, { resolve, reject })
728
- this.worker.postMessage({ id, filePath })
757
+ const onProgress = typeof options.onProgress === "function" ? options.onProgress : null
758
+ const job = {
759
+ worker,
760
+ resolve,
761
+ reject,
762
+ reportProgress: (bytes) => {
763
+ if (!onProgress) return
764
+ // Progress is observational and must never fail a content hash.
765
+ try { onProgress(bytes) } catch (_) {}
766
+ },
767
+ inactivityTimer: null,
768
+ resetInactivity: null
769
+ }
770
+ job.resetInactivity = () => {
771
+ clearTimeout(job.inactivityTimer)
772
+ const inactivityMs = Math.max(1, Number(this.hashInactivityMs) || HASH_INACTIVITY_MS)
773
+ job.inactivityTimer = setTimeout(() => {
774
+ const failure = new Error(`Timed out while reading ${path.basename(filePath)}`)
775
+ failure.code = "ETIMEDOUT"
776
+ this.failHashWorker(worker, failure, true)
777
+ }, inactivityMs)
778
+ if (job.inactivityTimer.unref) job.inactivityTimer.unref()
779
+ }
780
+ this.workerJobs.set(id, job)
781
+ job.resetInactivity()
782
+ try {
783
+ worker.postMessage({ id, filePath })
784
+ } catch (error) {
785
+ this.failHashWorker(worker, error, true)
786
+ }
729
787
  })
730
788
  }
731
789
  async refreshLinkSnapshots(hash, dev, ino) {
@@ -1798,6 +1856,8 @@ class Vault {
1798
1856
  : sweeper.state
1799
1857
  return Object.assign({}, state, {
1800
1858
  current_file: sweeper.currentHash ? path.basename(sweeper.currentHash.path) : null,
1859
+ current_file_bytes: sweeper.currentHash ? sweeper.currentHash.bytes : null,
1860
+ current_file_size: sweeper.currentHash ? sweeper.currentHash.size : null,
1801
1861
  pending,
1802
1862
  error: this.scanError
1803
1863
  })
@@ -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.47",
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
+ }