| // SPDX-License-Identifier: MIT |
| /* |
| * Copyright © 2019 Intel Corporation |
| */ |
| |
| #include <linux/kobject.h> |
| #include <linux/sysfs.h> |
| |
| #include "i915_drv.h" |
| #include "intel_engine.h" |
| #include "intel_engine_heartbeat.h" |
| #include "sysfs_engines.h" |
| |
| struct kobj_engine { |
| struct kobject base; |
| struct intel_engine_cs *engine; |
| }; |
| |
| static struct intel_engine_cs *kobj_to_engine(struct kobject *kobj) |
| { |
| return container_of(kobj, struct kobj_engine, base)->engine; |
| } |
| |
| static ssize_t |
| name_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| return sysfs_emit(buf, "%s\n", kobj_to_engine(kobj)->name); |
| } |
| |
| static const struct kobj_attribute name_attr = |
| __ATTR(name, 0444, name_show, NULL); |
| |
| static ssize_t |
| class_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| return sysfs_emit(buf, "%d\n", kobj_to_engine(kobj)->uabi_class); |
| } |
| |
| static const struct kobj_attribute class_attr = |
| __ATTR(class, 0444, class_show, NULL); |
| |
| static ssize_t |
| inst_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| return sysfs_emit(buf, "%d\n", kobj_to_engine(kobj)->uabi_instance); |
| } |
| |
| static const struct kobj_attribute inst_attr = |
| __ATTR(instance, 0444, inst_show, NULL); |
| |
| static ssize_t |
| mmio_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| return sysfs_emit(buf, "0x%x\n", kobj_to_engine(kobj)->mmio_base); |
| } |
| |
| static const struct kobj_attribute mmio_attr = |
| __ATTR(mmio_base, 0444, mmio_show, NULL); |
| |
| static const char * const vcs_caps[] = { |
| [ilog2(I915_VIDEO_CLASS_CAPABILITY_HEVC)] = "hevc", |
| [ilog2(I915_VIDEO_AND_ENHANCE_CLASS_CAPABILITY_SFC)] = "sfc", |
| }; |
| |
| static const char * const vecs_caps[] = { |
| [ilog2(I915_VIDEO_AND_ENHANCE_CLASS_CAPABILITY_SFC)] = "sfc", |
| }; |
| |
| static ssize_t repr_trim(char *buf, ssize_t len) |
| { |
| /* Trim off the trailing space and replace with a newline */ |
| if (len > PAGE_SIZE) |
| len = PAGE_SIZE; |
| if (len > 0) |
| buf[len - 1] = '\n'; |
| |
| return len; |
| } |
| |
| static ssize_t |
| __caps_show(struct intel_engine_cs *engine, |
| unsigned long caps, char *buf, bool show_unknown) |
| { |
| const char * const *repr; |
| int count, n; |
| ssize_t len; |
| |
| switch (engine->class) { |
| case VIDEO_DECODE_CLASS: |
| repr = vcs_caps; |
| count = ARRAY_SIZE(vcs_caps); |
| break; |
| |
| case VIDEO_ENHANCEMENT_CLASS: |
| repr = vecs_caps; |
| count = ARRAY_SIZE(vecs_caps); |
| break; |
| |
| default: |
| repr = NULL; |
| count = 0; |
| break; |
| } |
| GEM_BUG_ON(count > BITS_PER_LONG); |
| |
| len = 0; |
| for_each_set_bit(n, &caps, show_unknown ? BITS_PER_LONG : count) { |
| if (n >= count || !repr[n]) { |
| if (GEM_WARN_ON(show_unknown)) |
| len += sysfs_emit_at(buf, len, "[%x] ", n); |
| } else { |
| len += sysfs_emit_at(buf, len, "%s ", repr[n]); |
| } |
| if (GEM_WARN_ON(len >= PAGE_SIZE)) |
| break; |
| } |
| return repr_trim(buf, len); |
| } |
| |
| static ssize_t |
| caps_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return __caps_show(engine, engine->uabi_capabilities, buf, true); |
| } |
| |
| static const struct kobj_attribute caps_attr = |
| __ATTR(capabilities, 0444, caps_show, NULL); |
| |
| static ssize_t |
| all_caps_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| return __caps_show(kobj_to_engine(kobj), -1, buf, false); |
| } |
| |
| static const struct kobj_attribute all_caps_attr = |
| __ATTR(known_capabilities, 0444, all_caps_show, NULL); |
| |
| static ssize_t |
| max_spin_store(struct kobject *kobj, struct kobj_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| unsigned long long duration, clamped; |
| int err; |
| |
| /* |
| * When waiting for a request, if is it currently being executed |
| * on the GPU, we busywait for a short while before sleeping. The |
| * premise is that most requests are short, and if it is already |
| * executing then there is a good chance that it will complete |
| * before we can setup the interrupt handler and go to sleep. |
| * We try to offset the cost of going to sleep, by first spinning |
| * on the request -- if it completed in less time than it would take |
| * to go sleep, process the interrupt and return back to the client, |
| * then we have saved the client some latency, albeit at the cost |
| * of spinning on an expensive CPU core. |
| * |
| * While we try to avoid waiting at all for a request that is unlikely |
| * to complete, deciding how long it is worth spinning is for is an |
| * arbitrary decision: trading off power vs latency. |
| */ |
| |
| err = kstrtoull(buf, 0, &duration); |
| if (err) |
| return err; |
| |
| clamped = intel_clamp_max_busywait_duration_ns(engine, duration); |
| if (duration != clamped) |
| return -EINVAL; |
| |
| WRITE_ONCE(engine->props.max_busywait_duration_ns, duration); |
| |
| return count; |
| } |
| |
| static ssize_t |
| max_spin_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->props.max_busywait_duration_ns); |
| } |
| |
| static const struct kobj_attribute max_spin_attr = |
| __ATTR(max_busywait_duration_ns, 0644, max_spin_show, max_spin_store); |
| |
| static ssize_t |
| max_spin_default(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->defaults.max_busywait_duration_ns); |
| } |
| |
| static const struct kobj_attribute max_spin_def = |
| __ATTR(max_busywait_duration_ns, 0444, max_spin_default, NULL); |
| |
| static ssize_t |
| timeslice_store(struct kobject *kobj, struct kobj_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| unsigned long long duration, clamped; |
| int err; |
| |
| /* |
| * Execlists uses a scheduling quantum (a timeslice) to alternate |
| * execution between ready-to-run contexts of equal priority. This |
| * ensures that all users (though only if they of equal importance) |
| * have the opportunity to run and prevents livelocks where contexts |
| * may have implicit ordering due to userspace semaphores. |
| */ |
| |
| err = kstrtoull(buf, 0, &duration); |
| if (err) |
| return err; |
| |
| clamped = intel_clamp_timeslice_duration_ms(engine, duration); |
| if (duration != clamped) |
| return -EINVAL; |
| |
| WRITE_ONCE(engine->props.timeslice_duration_ms, duration); |
| |
| if (execlists_active(&engine->execlists)) |
| set_timer_ms(&engine->execlists.timer, duration); |
| |
| return count; |
| } |
| |
| static ssize_t |
| timeslice_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->props.timeslice_duration_ms); |
| } |
| |
| static const struct kobj_attribute timeslice_duration_attr = |
| __ATTR(timeslice_duration_ms, 0644, timeslice_show, timeslice_store); |
| |
| static ssize_t |
| timeslice_default(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->defaults.timeslice_duration_ms); |
| } |
| |
| static const struct kobj_attribute timeslice_duration_def = |
| __ATTR(timeslice_duration_ms, 0444, timeslice_default, NULL); |
| |
| static ssize_t |
| stop_store(struct kobject *kobj, struct kobj_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| unsigned long long duration, clamped; |
| int err; |
| |
| /* |
| * When we allow ourselves to sleep before a GPU reset after disabling |
| * submission, even for a few milliseconds, gives an innocent context |
| * the opportunity to clear the GPU before the reset occurs. However, |
| * how long to sleep depends on the typical non-preemptible duration |
| * (a similar problem to determining the ideal preempt-reset timeout |
| * or even the heartbeat interval). |
| */ |
| |
| err = kstrtoull(buf, 0, &duration); |
| if (err) |
| return err; |
| |
| clamped = intel_clamp_stop_timeout_ms(engine, duration); |
| if (duration != clamped) |
| return -EINVAL; |
| |
| WRITE_ONCE(engine->props.stop_timeout_ms, duration); |
| return count; |
| } |
| |
| static ssize_t |
| stop_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->props.stop_timeout_ms); |
| } |
| |
| static const struct kobj_attribute stop_timeout_attr = |
| __ATTR(stop_timeout_ms, 0644, stop_show, stop_store); |
| |
| static ssize_t |
| stop_default(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->defaults.stop_timeout_ms); |
| } |
| |
| static const struct kobj_attribute stop_timeout_def = |
| __ATTR(stop_timeout_ms, 0444, stop_default, NULL); |
| |
| static ssize_t |
| preempt_timeout_store(struct kobject *kobj, struct kobj_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| unsigned long long timeout, clamped; |
| int err; |
| |
| /* |
| * After initialising a preemption request, we give the current |
| * resident a small amount of time to vacate the GPU. The preemption |
| * request is for a higher priority context and should be immediate to |
| * maintain high quality of service (and avoid priority inversion). |
| * However, the preemption granularity of the GPU can be quite coarse |
| * and so we need a compromise. |
| */ |
| |
| err = kstrtoull(buf, 0, &timeout); |
| if (err) |
| return err; |
| |
| clamped = intel_clamp_preempt_timeout_ms(engine, timeout); |
| if (timeout != clamped) |
| return -EINVAL; |
| |
| WRITE_ONCE(engine->props.preempt_timeout_ms, timeout); |
| |
| if (READ_ONCE(engine->execlists.pending[0])) |
| set_timer_ms(&engine->execlists.preempt, timeout); |
| |
| return count; |
| } |
| |
| static ssize_t |
| preempt_timeout_show(struct kobject *kobj, struct kobj_attribute *attr, |
| char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->props.preempt_timeout_ms); |
| } |
| |
| static const struct kobj_attribute preempt_timeout_attr = |
| __ATTR(preempt_timeout_ms, 0644, preempt_timeout_show, preempt_timeout_store); |
| |
| static ssize_t |
| preempt_timeout_default(struct kobject *kobj, struct kobj_attribute *attr, |
| char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->defaults.preempt_timeout_ms); |
| } |
| |
| static const struct kobj_attribute preempt_timeout_def = |
| __ATTR(preempt_timeout_ms, 0444, preempt_timeout_default, NULL); |
| |
| static ssize_t |
| heartbeat_store(struct kobject *kobj, struct kobj_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| unsigned long long delay, clamped; |
| int err; |
| |
| /* |
| * We monitor the health of the system via periodic heartbeat pulses. |
| * The pulses also provide the opportunity to perform garbage |
| * collection. However, we interpret an incomplete pulse (a missed |
| * heartbeat) as an indication that the system is no longer responsive, |
| * i.e. hung, and perform an engine or full GPU reset. Given that the |
| * preemption granularity can be very coarse on a system, the optimal |
| * value for any workload is unknowable! |
| */ |
| |
| err = kstrtoull(buf, 0, &delay); |
| if (err) |
| return err; |
| |
| clamped = intel_clamp_heartbeat_interval_ms(engine, delay); |
| if (delay != clamped) |
| return -EINVAL; |
| |
| err = intel_engine_set_heartbeat(engine, delay); |
| if (err) |
| return err; |
| |
| return count; |
| } |
| |
| static ssize_t |
| heartbeat_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->props.heartbeat_interval_ms); |
| } |
| |
| static const struct kobj_attribute heartbeat_interval_attr = |
| __ATTR(heartbeat_interval_ms, 0644, heartbeat_show, heartbeat_store); |
| |
| static ssize_t |
| heartbeat_default(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
| { |
| struct intel_engine_cs *engine = kobj_to_engine(kobj); |
| |
| return sysfs_emit(buf, "%lu\n", engine->defaults.heartbeat_interval_ms); |
| } |
| |
| static const struct kobj_attribute heartbeat_interval_def = |
| __ATTR(heartbeat_interval_ms, 0444, heartbeat_default, NULL); |
| |
| static void kobj_engine_release(struct kobject *kobj) |
| { |
| kfree(kobj); |
| } |
| |
| static const struct kobj_type kobj_engine_type = { |
| .release = kobj_engine_release, |
| .sysfs_ops = &kobj_sysfs_ops |
| }; |
| |
| static struct kobject * |
| kobj_engine(struct kobject *dir, struct intel_engine_cs *engine) |
| { |
| struct kobj_engine *ke; |
| |
| ke = kzalloc(sizeof(*ke), GFP_KERNEL); |
| if (!ke) |
| return NULL; |
| |
| kobject_init(&ke->base, &kobj_engine_type); |
| ke->engine = engine; |
| |
| if (kobject_add(&ke->base, dir, "%s", engine->name)) { |
| kobject_put(&ke->base); |
| return NULL; |
| } |
| |
| /* xfer ownership to sysfs tree */ |
| return &ke->base; |
| } |
| |
| static void add_defaults(struct kobj_engine *parent) |
| { |
| static const struct attribute * const files[] = { |
| &max_spin_def.attr, |
| &stop_timeout_def.attr, |
| #if CONFIG_DRM_I915_HEARTBEAT_INTERVAL |
| &heartbeat_interval_def.attr, |
| #endif |
| NULL |
| }; |
| struct kobj_engine *ke; |
| |
| ke = kzalloc(sizeof(*ke), GFP_KERNEL); |
| if (!ke) |
| return; |
| |
| kobject_init(&ke->base, &kobj_engine_type); |
| ke->engine = parent->engine; |
| |
| if (kobject_add(&ke->base, &parent->base, "%s", ".defaults")) { |
| kobject_put(&ke->base); |
| return; |
| } |
| |
| if (sysfs_create_files(&ke->base, files)) |
| return; |
| |
| if (intel_engine_has_timeslices(ke->engine) && |
| sysfs_create_file(&ke->base, ×lice_duration_def.attr)) |
| return; |
| |
| if (intel_engine_has_preempt_reset(ke->engine) && |
| sysfs_create_file(&ke->base, &preempt_timeout_def.attr)) |
| return; |
| } |
| |
| void intel_engines_add_sysfs(struct drm_i915_private *i915) |
| { |
| static const struct attribute * const files[] = { |
| &name_attr.attr, |
| &class_attr.attr, |
| &inst_attr.attr, |
| &mmio_attr.attr, |
| &caps_attr.attr, |
| &all_caps_attr.attr, |
| &max_spin_attr.attr, |
| &stop_timeout_attr.attr, |
| #if CONFIG_DRM_I915_HEARTBEAT_INTERVAL |
| &heartbeat_interval_attr.attr, |
| #endif |
| NULL |
| }; |
| |
| struct device *kdev = i915->drm.primary->kdev; |
| struct intel_engine_cs *engine; |
| struct kobject *dir; |
| |
| dir = kobject_create_and_add("engine", &kdev->kobj); |
| if (!dir) |
| return; |
| |
| for_each_uabi_engine(engine, i915) { |
| struct kobject *kobj; |
| |
| kobj = kobj_engine(dir, engine); |
| if (!kobj) |
| goto err_engine; |
| |
| if (sysfs_create_files(kobj, files)) |
| goto err_object; |
| |
| if (intel_engine_has_timeslices(engine) && |
| sysfs_create_file(kobj, ×lice_duration_attr.attr)) |
| goto err_engine; |
| |
| if (intel_engine_has_preempt_reset(engine) && |
| sysfs_create_file(kobj, &preempt_timeout_attr.attr)) |
| goto err_engine; |
| |
| add_defaults(container_of(kobj, struct kobj_engine, base)); |
| |
| if (0) { |
| err_object: |
| kobject_put(kobj); |
| err_engine: |
| dev_err(kdev, "Failed to add sysfs engine '%s'\n", |
| engine->name); |
| break; |
| } |
| } |
| } |