Reboot from Haiku resets ACPI GPU state on Asus 1015PN (Optimus)

I don’t know if trying on FreeBSD will give anything. Another factor is that old ACPI required shutdown to happen on the same cpu as the boot cpu.

I’m sorry, but not sure what do you mean. How come there is a shutdown and a boot CPU? There is only one CPU in the netbook. Am I missing something?

CPU cores is more correct. It is probably not an issue in this case then.

1 Like

Just a second… I have one physical CPU, but it has two cores (dual core), and four threads. Are you saying, that the flash memory/EC chip/whatever it is, might get reset if the shutdown sequence is initiated by/from a different core from the one that have initiated the boot up? If so, that is an interesting idea… Hmm… Though the process (both boot and shutdown) I suppose is the same, and it’s “hard wired” to the BIOS (?!), so even if they got different cores, I don’t see how would that affect the NVRAM settings… I’m just thinking out loud…

I assume you can disable SMP in the Haiku boot options menu, and check if that helps.

In theory yes. I think what happened is that some machines had a bios not designed for multi core operation. For example it would use non-atomic operations in interrupts, because if the interrupt is running on the cpu, surely, nothing else can be running there. But on a multicore machine you can have things running on another cpu.

The solution was to make sure all acpi calls and interrupts are done in the same place.

The shutdown process has the additional problem tqat it shuts down all cpu cores one by one. If you do it in the wrong order and shut down the one toat’s running the shutdown code before the others, your shutdown does not complete cleanly.

Anywaye I’d say the next step here would be to understand where the information about the active videocard is really stored. Then, we can start investigating what may lead to oâerwriting or corrupting that storage.

Yes, as I eluded here…

Already tried that. It did not help. But thanks.

Oh, I see. Now it makes more sense. One would think, that the whole computer and OS stuff is simple, or at least less intricate…

I agree. I just wish I would know how. Already tried to walk that path, not much to show for so far. It could be stored in a specific (e.g. custom/manufacturer) module within the BIOS ROM, or the VGA BIOS of the GPUs (I have extracted and identified those too, from the BIOS, for both Intel and Nvidia, but other than the human readable ascii characters, I couldn’t decipher anything), could be a dedicated non-volatile chip, or who kows what else. Not much to go on with. Or at least I don’t know how.

Well, I consulted with quite a few AI services (yeah, I know), because while I am familiar with some of the aspects of this investigation, it would take much more time to make the proper research and implement something meaningful.

So, based on the information from the decompiled DSDT, as well as on this bash script tried to “catch” the changes during OSGS calls (and some other revisions of this, which tried to communicate with the EC chip), the conclusion I came to so far, are these:

  • there is a dedicated EC chip on the board, the OperationRegion (ECOR, EmbeddedControl, ...) in the DSDT is definitive proof
  • the GPU setting is stored on the main BIOS flash chip. The ISMI call proves the setting is written via a secure BIOS routine to the NVRAM partition on that flash chip
  • something is also written to the EC chip on GPU selection (via acpi_call), but couldn’t identify it yet, working on it
  • the CMOS battery’s failure causes the BIOS to erase the setting. It’s the RTC failure that triggers the BIOS to overwrite its own NVRAM
  • (tied to the previous point) force rebooting from any OS - e.g. holding down the power button (even while in BIOS menu or bootloader) causes a BIOS reset
  • (tied to the previous two points) Haiku doesn’t make the necessary calls (presumably ACPI) from the OS during shutdown sequence, to make the GPU selection written to a non-volatile memory (presumably on the EC chip), the way Windows and Linux does. It does not have the specific Asus and NVIDIA/Intel drivers that know how to perform the special ACPI handshake the BIOS expects. When Haiku initiates a reboot, it sends a standard ACPI command, which the BIOS interprets as an “unmanaged” or generic reset, not the clean, vendor-specific one it’s looking for. Therefore, it resets the settings
  • CMOS Byte 0x31 stores the GPU configuration
GPU Mode OSGS Value CMOS 0x31 Binary Upper Nibble
Intel 1 0xD5 1101 0101 0xD
NVIDIA 2 0xE5 1110 0101 0xE
Optimus 3 0xF5 1111 0101 0xF
NVIDIA_Alt 0 0xE5 1110 0101 0xE

Take the above information with a big grain of salt, as neither the AIs, nor do I, knows what are we dealing with exactly, it’s all just educated guesses, albeit logical ones.

I will try to do something with the EC chip capture next. Initial cheking shows that something is written there too.

Edit: reason: update

I’ve managed to gather some more data, based on this shell script. The EC (Embedded Controller) does track and respond to GPU mode changes, but it doesn’t simply mirror CMOS 0x31. Instead, it maintains its own state information related to power management and thermal control.

Mode CMOS 0x31 Change
Intel 0xd5 (no change, already using this GPU)
NVIDIA 0xe5 +0x10
Optimus 0xf5 +0x10

Pattern: Each GPU mode switch increments CMOS 0x31 by exactly 0x10.

Always Change (All Modes)

Register Purpose (Likely) Pattern
0x2E Temperature/Power Varies with thermal state
0x30 Temperature/Power Varies with thermal state
0x34 Power State Consistent changes
0x43 Unknown Control Mode-dependent
0xFC Counter/Timer Increments
0xFD Counter/Timer Increments
0xFE Counter/Timer Increments

NVIDIA-Specific Changes

Register Value Significance
0x8C → 0xFC This matches DSDT address 0x248C!
0xA7 → 0xA3 Unknown purpose

Intel/Optimus-Specific Changes

Register Value Significance
0x50 0x34/0x35 Power control?
0x66 → 0x0F8D Thermal threshold?

Not sure if this is of any help for/to anyone, nor can I tell if the data is really correct, but it’s a step forward, in my opinion.

In regards of my own idea of a “softer/warmer” reboot process, and @pinaraf, and @tqh idea about that and the ACPI calls, here are my thoughts, as of now.

There isn’t really a “softer” ACPI reboot command I guess. The core issue isn’t the reboot command itself, but the state-saving actions that must happen before the physical reboot is triggered. The solution isn’t to find a different way to reboot, but to execute the necessary ACPI commands before the standard reboot process begins, or, right before calling the ACPI_RESET function.

From my understanding, Linux executes the _PTS (Prepare to Sleep) method during reboot/shutdown, which allows the BIOS to perform necessary housekeeping like writing the sleep type value to the embedded controller. It might also call _DSM (Device Specific Method).

Would an implementation of the _PTS control method be enough, to execute during shutdown with the sleep state value (5 for S5 shutdown)? Or is it already implemented? If not, it could solve a lot of issues, I think. Is this a path worth experimenting with?

The question is for anyone. Thank you.

I think _PTS might be missing, but it may be built into ACPICA, so it is best to read their spec. It is quite well written on what it does on boot and shutdown and what it expects the OS to do. I know there were a few things left to do, but I don’t remember anymore.

If you read the boot part be aware that we do not really follow their order, where ACPI and setting up handlers should be done as one the of the first things.

4 Likes

Hello again.

I have managed to analyze the source code a bit, regarding how the reboot/shutdown sequence is implemented, and I thought about two possible solutions (or maybe I should call them quick and dirty fixes).

One, is to change the acpi_shutdown() method, to call the same prepare_sleep_state() regardless of the desired action (reboot or shutdown), something like this:

static status_t
acpi_shutdown(bool rebootSystem)
{
	if (debug_debugger_running() || !are_interrupts_enabled())
		return B_ERROR;

	acpi_module_info* acpi;
	if (get_module(B_ACPI_MODULE_NAME, (module_info**)&acpi) != B_OK)
		return B_NOT_SUPPORTED;

	status_t status;
	if (rebootSystem) {
		status = acpi->prepare_sleep_state(ACPI_POWER_STATE_OFF, NULL, 0);
		if (status != B_OK) {
			dprintf("acpi_shutdown: Warning - prepare_sleep_state failed for reboot\n");
			// Continue anyway, as the reboot might still work
		}
		status = acpi->reboot();
	} else {
		status = acpi->prepare_sleep_state(ACPI_POWER_STATE_OFF, NULL, 0);
		if (status == B_OK) {
			//cpu_status state = disable_interrupts();
			status = acpi->enter_sleep_state(ACPI_POWER_STATE_OFF);
			//restore_interrupts(state);
		}
	}

	put_module(B_ACPI_MODULE_NAME);
	return status;
}

Or, a bit more consolidated:

static status_t
acpi_shutdown(bool rebootSystem)
{
	if (debug_debugger_running() || !are_interrupts_enabled())
		return B_ERROR;

	acpi_module_info* acpi;
	if (get_module(B_ACPI_MODULE_NAME, (module_info**)&acpi) != B_OK)
		return B_NOT_SUPPORTED;

	status_t status;
    status = acpi->prepare_sleep_state(ACPI_POWER_STATE_OFF, NULL, 0);
	if (rebootSystem) {
		if (status != B_OK) {
			dprintf("acpi_shutdown: Warning - prepare_sleep_state failed for reboot\n");
			// Continue anyway, as the reboot might still work
		}
		status = acpi->reboot();
	} else {
		if (status == B_OK) {
			//cpu_status state = disable_interrupts();
			status = acpi->enter_sleep_state(ACPI_POWER_STATE_OFF);
			//restore_interrupts(state);
		}
	}

	put_module(B_ACPI_MODULE_NAME);
	return status;
}

And the second idea is to call prepare_sleep_state() from within reboot() method inside the BusManager, like this:

status_t
reboot(void)
{
	ACPI_STATUS status;

	TRACE("reboot\n");

    // Run ACPI S5 preparation so firmware/EC see _PTS(5)
    // Most likely, this would mirror what other OSes do 
    // for persisting platform state (e.g., EC/CMOS, muxes) across warm reboot.
    // If _PTS is absent, ACPICA no-ops safely with AE_NOT_FOUND.
    (void)prepare_sleep_state(ACPI_POWER_STATE_OFF, NULL, 0);

	status = AcpiReset();
	if (status == AE_NOT_EXIST)
		return B_UNSUPPORTED;

	if (status != AE_OK) {
		ERROR("Reset failed, status = %d\n", status);
		return B_ERROR;
	}

	snooze(1000000);
	ERROR("Reset failed, timeout\n");
	return B_ERROR;
}

These are only logical conclusions, haven’t tried them yet. But I would like to know, what others think, and what issues might arise from (one of) these changes. As far as I go, I don’t see an issue with either solution, because it is simply re-using the existing classes/methods/objects/whatever.

With this kind of change, the only way to be sure is to try and see what happens

Which I just did. Neither “solution” works. Either I am doing something wrong, or this is not the whole solution. Compiled the iso, reformatted the partition, installed. Outcome: the same, on reboot, it reverts back to Nvidia (default dGPU), instead of staying on the iGPU (with which it was booted). :frowning:

Some progress… Played around once again with reboot() method inside the BusManager. The following code is very specific to/for my hardware (some information used from the Arch Wiki of my netbook):

static status_t
asus_save_gpu_state(uint32* saved_state)
{
	ACPI_STATUS status;
	ACPI_BUFFER buffer = { ACPI_ALLOCATE_BUFFER, NULL };
	ACPI_OBJECT_LIST args;
	ACPI_OBJECT arg;
	char path[] = "\\AMW0.DSTS";
	
	// Call \AMW0.DSTS 0x90013 to get current GPU state
	args.Count = 1;
	args.Pointer = &arg;
	arg.Type = ACPI_TYPE_INTEGER;
	arg.Integer.Value = 0x90013;
	
	status = AcpiEvaluateObject(NULL, path, &args, &buffer);
	if (ACPI_SUCCESS(status)) {
		ACPI_OBJECT* result = (ACPI_OBJECT*)buffer.Pointer;
		if (result && result->Type == ACPI_TYPE_INTEGER) {
			*saved_state = result->Integer.Value;
			dprintf("ACPI: Current GPU state: 0x%x\n", *saved_state);
			AcpiOsFree(buffer.Pointer);
			return B_OK;
		}
		if (buffer.Pointer)
			AcpiOsFree(buffer.Pointer);
	}
	
	return B_ERROR;
}

static status_t
asus_restore_gpu_state(uint32 state)
{
	ACPI_STATUS status;
	ACPI_OBJECT_LIST args;
	ACPI_OBJECT arg;
	char path[] = "\\OSGS";
	
	// Convert the state from DSTS format to OSGS format
	// DSTS returns 0x30001, 0x30002, or 0x30003
	// OSGS expects 1, 2, or 3
	uint32 osgs_value = state & 0x3;
	
	dprintf("ACPI: Restoring GPU state with \\OSGS %d\n", osgs_value);
	
	args.Count = 1;
	args.Pointer = &arg;
	arg.Type = ACPI_TYPE_INTEGER;
	arg.Integer.Value = osgs_value;
	
	status = AcpiEvaluateObject(NULL, path, &args, NULL);
	if (ACPI_SUCCESS(status)) {
		dprintf("ACPI: GPU state restored successfully\n");
		return B_OK;
	}
	
	dprintf("ACPI: Failed to restore GPU state: %s\n", AcpiFormatException(status));
	return B_ERROR;
}

// Modified reboot function with ASUS GPU state preservation
status_t
reboot(void)
{
	ACPI_STATUS status;
	uint32 gpu_state = 0;
	bool has_asus_gpu = false;
	
	TRACE("reboot\n");
	
	// Check if this is an ASUS system with the GPU switching methods
	ACPI_HANDLE osgs_handle, dsts_handle;
	char osgs_path[] = "\\OSGS";
	char dsts_path[] = "\\AMW0.DSTS";
	if (ACPI_SUCCESS(AcpiGetHandle(NULL, osgs_path, &osgs_handle)) &&
	    ACPI_SUCCESS(AcpiGetHandle(NULL, dsts_path, &dsts_handle))) {
		has_asus_gpu = true;
		dprintf("ACPI: ASUS GPU switching methods detected\n");
		
		// Save current GPU state
		if (asus_save_gpu_state(&gpu_state) == B_OK) {
			// Immediately set it again to ensure it's written
			asus_restore_gpu_state(gpu_state);
			
			// Give EC/CMOS time to write
			snooze(50000); // 50ms
		}
		else {
			dprintf("ACPI: asus_save_gpu_state is B_NOT_OK\n");
		}
	}
	else {
		dprintf("ACPI: ASUS GPU switching methods NOT detected\n");
	}
	
	// Call _PTS to prepare for sleep/shutdown
	// This should trigger BIOS to save various states
	prepare_sleep_state(ACPI_POWER_STATE_OFF, NULL, 0);
	
	// Extra wait for ASUS systems
	if (has_asus_gpu) {
		dprintf("ACPI: Waiting for ASUS EC to save state...\n");
		snooze(100000); // 100ms extra for EC writes
		
		// Call OSGS one more time to be absolutely sure
		if (gpu_state != 0) {
			asus_restore_gpu_state(gpu_state);
			snooze(50000); // Another 50ms
		}
		else {
			dprintf("ACPI: gpu_state is 0\n");
		}
	}
	else {
		dprintf("ACPI: has_asus_gpu is FALSE1\n");
	}
	
	dprintf("ACPI: Calling AcpiReset()\n");
	status = AcpiReset();
	if (status == AE_NOT_EXIST) {
		dprintf("ACPI: Reset method does not exist\n");
		return B_UNSUPPORTED;
	}
	if (status != AE_OK) {
		ERROR("Reset failed, status = %s\n", AcpiFormatException(status));
		return B_ERROR;
	}
	snooze(1000000);
	ERROR("Reset failed, timeout\n");
	return B_ERROR;
}

Checking /var/log/previous_syslog, I can see the following output:

ACPI: ASUS GPU switching methods detected
ACPI: Current GPU state: 0x30001
ACPI: Restoring GPU state with \OSGS 1
ACPI: GPU state restored successfully
ACPI: Waiting for ASUS EC to save state...
ACPI: Restoring GPU state with \OSGS 1
ACPI: GPU state restored successfully
ACPI: Calling AcpiReset()
ACPI: Reset method does not exist

This save state does the same as acpi_call from Linux, except it does directly on reboot (twice, to make sure). And it works (!!!). But as it is, I don’t think I could submit this. Have to figure out a better way, so it wouldn’t affect/mess-up other sysyems. Obviously any future haiku.pkg update will overwrite this, so leaving it the BusManager is not an option (unless I want to re-apply the “patch” and compile it every time).

As far as I’m concerned, this already does what I needed, from this point on is just a matter of applying it in a more convenient way…

3 Likes

Just a note on shutdown: the current way should follow what (an older) ACPICA specs say you should do on shutdown and in what order, so I don’t think prepare sleep should be changed.
It is better to follow the spec, and as you already have started on doing, add what ever vendor specific voodoo is missing. Perhaps a newer version of the spec has more info on boot/shutdown steps that trigger vendor specific actions though in windows there is usually a vendor driver for every part…

2 Likes

I am one hundred percent agree with you. The above code is more like a proof of concept, rather than a final implementation, just to see if this could work. Tought long and hard about what’s next for me, and came to the conclusion, that I will not submit any changes to core components.

First of all, while I am surely not alone with an EEE PC with dual GPU, also aware that the vast majority of the users doesn’t need this, so implementing in any core component would be rather a bad move, inefficient. I wouldn’t like it either, if something would be “forced down on my throath”, while at the same time wouldn’t benefit from it. And also would be against Haiku’s phylosophy. So it has to be optional.

Second of all, such ACPI requests cannot be done purely in userland. So, instead of messing with the already well thought out components and implemented ways (e.g. reboot/shutdown), I think a clean way is a tiny kernel driver that exposes just the two ACPI calls I need via ioctls, plus a small CLI tool that invokes it before reboot. That would keep the logic out of the kernel core, survive system updates, and would let anyone automate it with a wrapper script or a background helper. And could be maintained/extended separately/independently.

These are my thoughts, and the theory. Now good luck to me with the implementation. :slight_smile:

1 Like

Do you mean EFI runtime services? EFI boot services are not available after boot loader started kernel because ExitBootServices() API is required to be called.

1 Like

The question is, how early before reboot you can get away with it.

If it has to be done between prepare shutdown and reboot, this may be quite late in the shutdown process where userspace is already completely stopped. In that case, no way to run a custom command.

What would make more sense to me intuitively is using the “shutdown” hook of a driver (maybe writing a specific ACPI driver just for this), but I think the “shutdown” hook is not currently invoked by the kernel. So we would need to implement that, unless the acpi calls can be run at any time

Well… That’s the beauty of it. By my calculations, in my case, ANYTIME, before reboot/shutdown, literally. Sure, I might be wrong, but if I manage to pull this off, and replicate what Windows and Linux does, then the timing actually becomes irrelevant. As long as I manage to make the necessary ACPI calls, which tells the BIOS to do its thing in regards to the selected GPU for next boot (be it warm boot, or full power off/on), I’m golden. Was so fixated about timing and methods and hooking it in some specific way, at a specific location in the chain, that I almost lost sight, what I am after. And that is: to make sure, both the BIOS and the EC chip gets written. And this is done by calling (the proprietary) OSGS method from the DSDT. One can dream at least…